-
Notifications
You must be signed in to change notification settings - Fork 15
BOOM API: some more guard rails #400
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
base: main
Are you sure you want to change the base?
Changes from all commits
83d9599
00e67b5
9afdd76
cd74709
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 |
|---|---|---|
|
|
@@ -66,7 +66,7 @@ struct ConeSearchQuery { | |
| radius: f64, | ||
| unit: Unit, | ||
| object_coordinates: HashMap<String, [f64; 2]>, // Map of catalog name to coordinates [RA, Dec] | ||
| limit: Option<i64>, | ||
| limit: u32, | ||
| skip: Option<u64>, | ||
| sort: Option<serde_json::Value>, | ||
| max_time_ms: Option<u64>, | ||
|
|
@@ -86,9 +86,13 @@ impl ConeSearchQuery { | |
| } | ||
| } | ||
| } | ||
| if let Some(limit) = self.limit { | ||
| options.limit = Some(limit); | ||
| // assert that limit is a positive integer < 100_000 | ||
| if self.limit == 0 || self.limit > 100_000 { | ||
| return Err( | ||
| "Limit must be a positive integer less than or equal to 100,000".to_string(), | ||
|
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. Why not set a default limit as 100,000?
Collaborator
Author
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. because when pagination uses defaults, it means the user may get up to default and think we got all the data, if they don't know their is pagination (and my experience showed me they never ever THINK about it). It's too prone to error. This forces them to be aware of it, because they have to use it. |
||
| ); | ||
| } | ||
| options.limit = Some(self.limit as i64); | ||
| if let Some(skip) = self.skip { | ||
| options.skip = Some(skip); | ||
| } | ||
|
|
@@ -102,6 +106,8 @@ impl ConeSearchQuery { | |
| } | ||
| if let Some(max_time_ms) = self.max_time_ms { | ||
| options.max_time = Some(std::time::Duration::from_millis(max_time_ms)); | ||
| } else { | ||
| options.max_time = Some(std::time::Duration::from_secs(30)); // Default max time | ||
| } | ||
| Ok(options) | ||
| } | ||
|
|
@@ -146,6 +152,12 @@ pub async fn post_cone_search_query( | |
| Unit::Radians => {} | ||
| } | ||
| let object_coordinates = &body.object_coordinates; | ||
| if object_coordinates.is_empty() || object_coordinates.len() > 10_000 { | ||
| return response::bad_request( | ||
| "Invalid number of coordinate pairs, must be between 1 and 10,000", | ||
| ); | ||
| } | ||
|
Comment on lines
+155
to
+159
|
||
|
|
||
| let mut docs: HashMap<String, Vec<mongodb::bson::Document>> = HashMap::new(); | ||
| let filter = match parse_optional_filter(&body.filter) { | ||
| Ok(f) => f, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,14 +12,18 @@ use utoipa::ToSchema; | |
| struct PipelineQuery { | ||
| catalog_name: String, | ||
| pipeline: serde_json::Value, | ||
| limit: u32, | ||
| skip: Option<u64>, | ||
| max_time_ms: Option<u64>, | ||
|
Theodlz marked this conversation as resolved.
|
||
| } | ||
|
Theodlz marked this conversation as resolved.
|
||
| impl PipelineQuery { | ||
| /// Convert to MongoDB Find options | ||
| /// Convert to MongoDB Aggregation options | ||
| fn to_pipeline_options(&self) -> mongodb::options::AggregateOptions { | ||
| let mut options = mongodb::options::AggregateOptions::default(); | ||
| if let Some(max_time_ms) = self.max_time_ms { | ||
| options.max_time = Some(std::time::Duration::from_millis(max_time_ms)); | ||
| } else { | ||
| options.max_time = Some(std::time::Duration::from_secs(30)); // Default max time | ||
| } | ||
| options | ||
| } | ||
|
|
@@ -52,8 +56,25 @@ pub async fn post_pipeline_query( | |
| // Find documents with the provided filter | ||
| let pipeline = match parse_pipeline(&body.pipeline) { | ||
| Ok(pipeline) => pipeline, | ||
| Err(e) => return response::bad_request(&format!("Invalid filter: {}", e)), | ||
| Err(e) => return response::bad_request(&format!("Invalid pipeline: {}", e)), | ||
| }; | ||
| let mut pipeline = pipeline; | ||
|
|
||
| // add a skip stage to the pipeline if skip is set (must come before $limit) | ||
| if let Some(skip) = body.skip { | ||
| let skip_stage = doc! { "$skip": skip as i64 }; | ||
| pipeline.push(skip_stage); | ||
| } | ||
| // add a limit stage to the pipeline after validating that the limit is a positive integer < 100_000 | ||
| if body.limit == 0 || body.limit > 100_000 { | ||
| return response::bad_request( | ||
| "Limit must be a positive integer less than or equal to 100,000", | ||
| ); | ||
| } | ||
|
|
||
| let limit_stage = doc! { "$limit": body.limit }; | ||
| pipeline.push(limit_stage); | ||
|
Comment on lines
+63
to
+76
|
||
|
|
||
| let pipeline_options = body.to_pipeline_options(); | ||
| let mut cursor = match collection | ||
| .aggregate(pipeline) | ||
|
|
||
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.
I think you can do something like this for the docs:
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.
I think you can also do something like
#[validate(range(min = 1, max = 100_000))]and callbody.validate()in the endpoint function to catch validation errors. Might be nicer to declare on the structs rather than putting the logic in the functions.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.
I used utoipa for that before. I like setting a min max in the schema for documentation. But for validation, I had terrible results in another project. The user gets these standardized but opaque deserialization error, and they don't understand what went wrong in most cases. I definitely want to use
#[utoipa(schema(...))]everywhere in the API (might deserve its own ticket but let me give a go at it now), but the validation isn't a good idea in my opinion, as elegant as it sounds like from the developer's perspective.