-
Notifications
You must be signed in to change notification settings - Fork 0
Harden input validation for empty/whitespace string fields across request builders and type constructors #14
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 2 commits
6816f1c
2d522ac
f3d05b1
db3ef42
745601b
dfeb732
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 |
|---|---|---|
|
|
@@ -18,13 +18,21 @@ pub struct RelationshipFilter { | |
|
|
||
| impl RelationshipFilter { | ||
| /// Creates a new filter for the given resource type. | ||
| pub fn new(resource_type: impl Into<String>) -> Self { | ||
| Self { | ||
| resource_type: resource_type.into(), | ||
| /// | ||
| /// Returns `Err` if `resource_type` is empty or whitespace-only. | ||
| pub fn new(resource_type: impl Into<String>) -> Result<Self, Error> { | ||
| let resource_type = resource_type.into(); | ||
|
Comment on lines
20
to
+24
|
||
| if resource_type.trim().is_empty() { | ||
| return Err(Error::InvalidArgument( | ||
| "resource_type must not be empty".into(), | ||
| )); | ||
| } | ||
| Ok(Self { | ||
| resource_type, | ||
| optional_resource_id: None, | ||
| optional_relation: None, | ||
| optional_subject_filter: None, | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| /// Adds a resource ID filter. | ||
|
|
@@ -71,12 +79,20 @@ pub struct SubjectFilter { | |
|
|
||
| impl SubjectFilter { | ||
| /// Creates a new subject filter for the given type. | ||
| pub fn new(subject_type: impl Into<String>) -> Self { | ||
| Self { | ||
| subject_type: subject_type.into(), | ||
| /// | ||
| /// Returns `Err` if `subject_type` is empty or whitespace-only. | ||
| pub fn new(subject_type: impl Into<String>) -> Result<Self, Error> { | ||
|
Comment on lines
100
to
+104
|
||
| let subject_type = subject_type.into(); | ||
| if subject_type.trim().is_empty() { | ||
| return Err(Error::InvalidArgument( | ||
| "subject_type must not be empty".into(), | ||
| )); | ||
| } | ||
| Ok(Self { | ||
| subject_type, | ||
| optional_subject_id: None, | ||
| optional_relation: None, | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| /// Adds a subject ID filter. | ||
|
|
@@ -133,3 +149,44 @@ impl ReadRelationshipResult { | |
| }) | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn relationship_filter_valid() { | ||
| let f = RelationshipFilter::new("document").unwrap(); | ||
| assert_eq!(f.resource_type, "document"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn relationship_filter_empty_type_rejected() { | ||
| let err = RelationshipFilter::new("").unwrap_err(); | ||
| assert!(matches!(err, Error::InvalidArgument(_))); | ||
| } | ||
|
|
||
| #[test] | ||
| fn relationship_filter_whitespace_type_rejected() { | ||
| let err = RelationshipFilter::new(" ").unwrap_err(); | ||
| assert!(matches!(err, Error::InvalidArgument(_))); | ||
| } | ||
|
|
||
| #[test] | ||
| fn subject_filter_valid() { | ||
| let f = SubjectFilter::new("user").unwrap(); | ||
| assert_eq!(f.subject_type, "user"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn subject_filter_empty_type_rejected() { | ||
| let err = SubjectFilter::new("").unwrap_err(); | ||
| assert!(matches!(err, Error::InvalidArgument(_))); | ||
| } | ||
|
|
||
| #[test] | ||
| fn subject_filter_whitespace_type_rejected() { | ||
| let err = SubjectFilter::new(" ").unwrap_err(); | ||
| assert!(matches!(err, Error::InvalidArgument(_))); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,11 +16,19 @@ pub struct Caveat { | |
|
|
||
| impl Caveat { | ||
| /// Creates a new caveat with the given name and context. | ||
| pub fn new(name: impl Into<String>, context: HashMap<String, ContextValue>) -> Self { | ||
| Self { | ||
| name: name.into(), | ||
| context, | ||
| /// | ||
| /// Returns `Err` if `name` is empty or whitespace-only. | ||
| pub fn new(name: impl Into<String>, context: HashMap<String, ContextValue>) -> Result<Self, Error> { | ||
| let name = name.into(); | ||
| if name.trim().is_empty() { | ||
| return Err(Error::InvalidArgument( | ||
| "caveat name must not be empty".into(), | ||
| )); | ||
| } | ||
| Ok(Self { | ||
| name, | ||
| context, | ||
| }) | ||
| } | ||
|
Comment on lines
17
to
32
|
||
| } | ||
|
|
||
|
|
@@ -39,17 +47,25 @@ pub struct Relationship { | |
|
|
||
| impl Relationship { | ||
| /// Creates a new relationship without a caveat. | ||
| /// | ||
| /// Returns `Err` if `relation` is empty or whitespace-only. | ||
| pub fn new( | ||
| resource: ObjectReference, | ||
| relation: impl Into<String>, | ||
| subject: SubjectReference, | ||
| ) -> Self { | ||
| Self { | ||
| ) -> Result<Self, Error> { | ||
| let relation = relation.into(); | ||
| if relation.trim().is_empty() { | ||
| return Err(Error::InvalidArgument( | ||
| "relation must not be empty".into(), | ||
| )); | ||
| } | ||
| Ok(Self { | ||
| resource, | ||
| relation: relation.into(), | ||
| relation, | ||
| subject, | ||
| optional_caveat: None, | ||
| } | ||
| }) | ||
| } | ||
|
Comment on lines
48
to
69
|
||
|
|
||
| /// Attaches a caveat to this relationship. | ||
|
|
@@ -259,7 +275,8 @@ mod tests { | |
| None::<String>, | ||
| ) | ||
| .unwrap(), | ||
| ); | ||
| ) | ||
| .unwrap(); | ||
| let update = RelationshipUpdate::create(rel); | ||
| assert_eq!(update.operation, Operation::Create); | ||
| } | ||
|
|
@@ -275,15 +292,58 @@ mod tests { | |
| ) | ||
| .unwrap(), | ||
| ) | ||
| .with_caveat(Caveat::new("ip_check", HashMap::new())); | ||
| .unwrap() | ||
| .with_caveat(Caveat::new("ip_check", HashMap::new()).unwrap()); | ||
| assert!(rel.optional_caveat.is_some()); | ||
| assert_eq!(rel.optional_caveat.unwrap().name, "ip_check"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn relationship_empty_relation_rejected() { | ||
| let err = Relationship::new( | ||
| ObjectReference::new("doc", "1").unwrap(), | ||
| "", | ||
| SubjectReference::new( | ||
| ObjectReference::new("user", "alice").unwrap(), | ||
| None::<String>, | ||
| ) | ||
| .unwrap(), | ||
| ) | ||
| .unwrap_err(); | ||
| assert!(matches!(err, Error::InvalidArgument(_))); | ||
| } | ||
|
|
||
| #[test] | ||
| fn relationship_whitespace_relation_rejected() { | ||
| let err = Relationship::new( | ||
| ObjectReference::new("doc", "1").unwrap(), | ||
| " ", | ||
| SubjectReference::new( | ||
| ObjectReference::new("user", "alice").unwrap(), | ||
| None::<String>, | ||
| ) | ||
| .unwrap(), | ||
| ) | ||
| .unwrap_err(); | ||
| assert!(matches!(err, Error::InvalidArgument(_))); | ||
| } | ||
|
|
||
| #[test] | ||
| fn caveat_empty_name_rejected() { | ||
| let err = Caveat::new("", HashMap::new()).unwrap_err(); | ||
| assert!(matches!(err, Error::InvalidArgument(_))); | ||
| } | ||
|
|
||
| #[test] | ||
| fn caveat_whitespace_name_rejected() { | ||
| let err = Caveat::new(" ", HashMap::new()).unwrap_err(); | ||
| assert!(matches!(err, Error::InvalidArgument(_))); | ||
| } | ||
|
|
||
| #[test] | ||
| fn precondition_must_exist() { | ||
| use crate::types::RelationshipFilter; | ||
| let p = Precondition::must_exist(RelationshipFilter::new("document")); | ||
| let p = Precondition::must_exist(RelationshipFilter::new("document").unwrap()); | ||
| assert_eq!(p.operation, PreconditionOp::MustExist); | ||
| } | ||
| } | ||
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.
The same
trim().is_empty()validation (with identical error strings) is now duplicated across multiple request builders in this module. Consider factoring this into a small helper (e.g.,validate_non_empty(field, value)) to avoid message drift and to keep future validations consistent.