-
Notifications
You must be signed in to change notification settings - Fork 1
Standardize rule endpoint client identification #643
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
312429d
6d4fe02
771f123
e32c342
46100f9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,27 +21,24 @@ export interface RulesResponse { | |
| client_rules: ClientRule[]; | ||
| } | ||
|
|
||
| const basicAuthHeader = (clientId: string): string => 'Basic ' + btoa(`${clientId}:`); | ||
| const clientIdHeader = (clientId: string): Record<string, string> => ({ 'X-Client-Id': clientId }); | ||
|
|
||
| export const getRulesByClientId = (clientId: string): Promise<Response> => { | ||
| return fetch(urls.getRulesByClientId(clientId)); | ||
| return fetch(urls.getRulesWithSubscriptionStatus(), { headers: clientIdHeader(clientId) }); | ||
| }; | ||
|
|
||
| export const addRule = (clientId: string, rule: RulePayload): Promise<Response> => { | ||
| return fetch(urls.addRule(), { | ||
| method: 'PUT', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| Authorization: basicAuthHeader(clientId) | ||
| }, | ||
| headers: { 'Content-Type': 'application/json', ...clientIdHeader(clientId) }, | ||
| body: JSON.stringify(rule) | ||
| }); | ||
| }; | ||
|
|
||
| export const deleteRule = (clientId: string, ruleId: string): Promise<Response> => { | ||
| return fetch(urls.deleteRule(ruleId), { | ||
| method: 'POST', | ||
| headers: { Authorization: basicAuthHeader(clientId) } | ||
| headers: clientIdHeader(clientId) | ||
| }); | ||
| }; | ||
|
|
||
|
|
@@ -53,23 +50,18 @@ export const editRule = (ruleId: string, rule: object): Promise<Response> => { | |
| }); | ||
| }; | ||
|
|
||
| export interface RuleSubscriptionRequest { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in the future a type like this could be good, but rn no abstraction needed. |
||
| rule_ids: string[]; | ||
| client_id: string; | ||
| } | ||
|
|
||
| export const subscribeToRules = (request: RuleSubscriptionRequest): Promise<Response> => { | ||
| export const subscribeToRules = (clientId: string, ruleIds: string[]): Promise<Response> => { | ||
| return fetch(urls.subscribeToRule(), { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify(request) | ||
| headers: { 'Content-Type': 'application/json', ...clientIdHeader(clientId) }, | ||
| body: JSON.stringify(ruleIds) | ||
| }); | ||
| }; | ||
|
|
||
| export const unsubscribeFromRules = (request: RuleSubscriptionRequest): Promise<Response> => { | ||
| export const unsubscribeFromRules = (clientId: string, ruleIds: string[]): Promise<Response> => { | ||
| return fetch(urls.unsubscribeFromRule(), { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify(request) | ||
| headers: { 'Content-Type': 'application/json', ...clientIdHeader(clientId) }, | ||
| body: JSON.stringify(ruleIds) | ||
| }); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -73,9 +73,11 @@ export class RulesTableComponent implements OnInit { | |
| } | ||
|
|
||
| async onToggleSubscription(rule: ClientRule, subscribed: boolean): Promise<void> { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. a follow up ticket. I didn't really investigate the quality of toggle subscriptions when initially made. I will make this a ticket. |
||
| const request = { rule_ids: [rule.id], client_id: this.clientId() }; | ||
| const ruleIds = [rule.id]; | ||
| try { | ||
| const response = subscribed ? await subscribeToRules(request) : await unsubscribeFromRules(request); | ||
| const response = subscribed | ||
| ? await subscribeToRules(this.clientId(), ruleIds) | ||
| : await unsubscribeFromRules(this.clientId(), ruleIds); | ||
| if (response.ok) { | ||
| this.rules.update((rules) => rules.map((r) => (r.id === rule.id ? { ...r, is_subscribed: subscribed } : r))); | ||
| } else { | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,9 @@ | ||
| use std::sync::Arc; | ||
|
|
||
| use axum::{Extension, Json, debug_handler, extract::Path}; | ||
| use axum_extra::{ | ||
| TypedHeader, | ||
| headers::{Authorization, authorization::Basic}, | ||
| use axum::{ | ||
| Extension, Json, debug_handler, | ||
| extract::{FromRequestParts, Path}, | ||
| http::{StatusCode, request::Parts}, | ||
| }; | ||
| use serde::Deserialize; | ||
| use serde_with::DurationSeconds; | ||
|
|
@@ -16,47 +16,53 @@ use crate::{ | |
| rule_structs::{ClientId, Rule, RuleId, RuleManager, RulesResponse}, | ||
| }; | ||
|
|
||
| #[derive(Deserialize)] | ||
| pub struct RuleSubscriptionRequest { | ||
| rule_ids: Vec<String>, | ||
| client_id: String, | ||
| const CLIENT_ID_HEADER: &str = "x-client-id"; | ||
|
|
||
| /// client id comes from the x-client-id header, keeping it out of conflict-prone route paths | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you link to rust docs on this. like to keep everything more complex to the docs. |
||
| /// extractor pattern: <https://docs.rs/axum/latest/axum/extract/index.html#defining-custom-extractors> | ||
| impl<S> FromRequestParts<S> for ClientId | ||
| where | ||
| S: Send + Sync, | ||
| { | ||
| type Rejection = ScyllaError; | ||
|
|
||
| async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> { | ||
| parts | ||
| .headers | ||
| .get(CLIENT_ID_HEADER) | ||
| .and_then(|value| value.to_str().ok()) | ||
| .filter(|value| !value.is_empty()) | ||
| .map(|value| ClientId(value.to_owned())) | ||
| .ok_or_else(|| { | ||
| ScyllaError::HttpError( | ||
| StatusCode::BAD_REQUEST, | ||
| format!("Missing or empty {CLIENT_ID_HEADER} header"), | ||
| ) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| #[debug_handler] | ||
| pub async fn add_rule( | ||
| TypedHeader(auth): TypedHeader<Authorization<Basic>>, | ||
| client_id: ClientId, | ||
| Extension(rules_manager): Extension<Arc<RuleManager>>, | ||
| Json(rule): Json<Rule>, | ||
| ) -> Result<Json<String>, ScyllaError> { | ||
| info!( | ||
| "Incoming rules reg: {}, from {}", | ||
| rule.topic, | ||
| auth.username().to_string() | ||
| ); | ||
| match rules_manager | ||
| .add_rule(ClientId(auth.username().to_string()), rule) | ||
| .await | ||
| { | ||
| info!("Incoming rules reg: {}, from {}", rule.topic, client_id); | ||
| match rules_manager.add_rule(client_id, rule).await { | ||
| Ok(_) => Ok(Json::from("Rule added!".to_owned())), | ||
| Err(err) => Err(ScyllaError::RuleError(err)), | ||
| } | ||
| } | ||
|
|
||
| #[debug_handler] | ||
| pub async fn delete_rule( | ||
| TypedHeader(auth): TypedHeader<Authorization<Basic>>, | ||
| Extension(rules_manager): Extension<Arc<RuleManager>>, | ||
| client_id: ClientId, | ||
| Path(rule_id): Path<String>, | ||
| Extension(rules_manager): Extension<Arc<RuleManager>>, | ||
| ) -> Result<(), ScyllaError> { | ||
| info!( | ||
| "Incoming rules del: {}, from {}", | ||
| rule_id, | ||
| auth.username().to_string() | ||
| ); | ||
| match rules_manager | ||
| .delete_rule(ClientId(auth.username().to_string()), RuleId(rule_id)) | ||
| .await | ||
| { | ||
| info!("Incoming rules del: {}, from {}", rule_id, client_id); | ||
| match rules_manager.delete_rule(client_id, RuleId(rule_id)).await { | ||
| Ok(_) => Ok(()), | ||
| Err(err) => Err(ScyllaError::RuleError(err)), | ||
| } | ||
|
|
@@ -72,22 +78,22 @@ pub async fn get_all_rules( | |
|
|
||
| #[debug_handler] | ||
| pub async fn get_client_subscribed_rules( | ||
| Path(client_id): Path<String>, | ||
| client_id: ClientId, | ||
| Extension(rules_manager): Extension<Arc<RuleManager>>, | ||
| ) -> Json<Vec<Rule>> { | ||
| debug!("Fetching subscribed rules for client {}", client_id); | ||
| Json(rules_manager.get_client_rules(ClientId(client_id)).await) | ||
| Json(rules_manager.get_client_rules(client_id).await) | ||
| } | ||
|
|
||
| #[debug_handler] | ||
| pub async fn get_all_rules_with_client_info( | ||
| Path(client_id): Path<String>, | ||
| client_id: ClientId, | ||
| Extension(rules_manager): Extension<Arc<RuleManager>>, | ||
| ) -> Result<Json<RulesResponse>, ScyllaError> { | ||
| debug!("Fetching all rules"); | ||
| Ok(Json( | ||
| rules_manager | ||
| .get_all_rules_with_subscription_status(ClientId(client_id)) | ||
| .get_all_rules_with_subscription_status(client_id) | ||
| .await, | ||
| )) | ||
| } | ||
|
|
@@ -126,21 +132,19 @@ pub async fn edit_rule( | |
|
|
||
| #[debug_handler] | ||
| pub async fn unsubscribe_rules( | ||
| client_id: ClientId, | ||
| Extension(rules_manager): Extension<Arc<RuleManager>>, | ||
| Json(request): Json<RuleSubscriptionRequest>, | ||
| Json(rule_ids): Json<Vec<String>>, | ||
| ) -> Result<Json<String>, ScyllaError> { | ||
| info!( | ||
| "Unsubscribing client {} from {} rules", | ||
| request.client_id, | ||
| request.rule_ids.len() | ||
| client_id, | ||
| rule_ids.len() | ||
| ); | ||
|
|
||
| let rule_ids: Vec<RuleId> = request.rule_ids.into_iter().map(RuleId).collect(); | ||
| let rule_ids: Vec<RuleId> = rule_ids.into_iter().map(RuleId).collect(); | ||
|
|
||
| match rules_manager | ||
| .unsubscribe_rules(ClientId(request.client_id), rule_ids) | ||
| .await | ||
| { | ||
| match rules_manager.unsubscribe_rules(client_id, rule_ids).await { | ||
| Ok(_) => Ok(Json::from( | ||
| "Successfully unsubscribed from rules".to_owned(), | ||
| )), | ||
|
|
@@ -150,21 +154,19 @@ pub async fn unsubscribe_rules( | |
|
|
||
| #[debug_handler] | ||
| pub async fn subscribe_rules( | ||
| client_id: ClientId, | ||
| Extension(rules_manager): Extension<Arc<RuleManager>>, | ||
| Json(request): Json<RuleSubscriptionRequest>, | ||
| Json(rule_ids): Json<Vec<String>>, | ||
| ) -> Result<Json<String>, ScyllaError> { | ||
| info!( | ||
| "Subscribing client {} to {} rules", | ||
| request.client_id, | ||
| request.rule_ids.len() | ||
| client_id, | ||
| rule_ids.len() | ||
| ); | ||
|
|
||
| let rule_ids: Vec<RuleId> = request.rule_ids.into_iter().map(RuleId).collect(); | ||
| let rule_ids: Vec<RuleId> = rule_ids.into_iter().map(RuleId).collect(); | ||
|
|
||
| match rules_manager | ||
| .subscribe_rules(ClientId(request.client_id), rule_ids) | ||
| .await | ||
| { | ||
| match rules_manager.subscribe_rules(client_id, rule_ids).await { | ||
| Ok(_) => Ok(Json::from("Successfully subscribed to rules".to_owned())), | ||
| Err(err) => Err(ScyllaError::RuleError(err)), | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For another ticket, task we can make: transitioning to a slightly more abstracted tool than fetch, or further abstracting headers into multiple lambdas or a recursive lambda setup... something along those lines.