@@ -19,8 +19,7 @@ pub enum PatchRequestError {
1919#[ serde( tag = "op" ) ]
2020pub enum PatchOp {
2121 #[ serde( rename = "replace" ) ]
22- // NB: replace ops have optional paths but we don't support it yet
23- Replace { value : serde_json:: Value } ,
22+ Replace { path : Option < String > , value : serde_json:: Value } ,
2423 #[ serde( rename = "add" ) ]
2524 Add { path : Option < String > , value : serde_json:: Value } ,
2625 #[ serde( rename = "remove" ) ]
@@ -58,7 +57,7 @@ impl PatchRequest {
5857 let mut updated_user = stored_user. clone ( ) ;
5958
6059 for patch_op in & self . operations {
61- let PatchOp :: Replace { value } = patch_op else {
60+ let PatchOp :: Replace { path : _ , value } = patch_op else {
6261 return Err ( PatchRequestError :: Unsupported (
6362 "only the replace op is supported for users" . to_string ( ) ,
6463 ) ) ;
@@ -94,9 +93,11 @@ impl PatchRequest {
9493
9594 for patch_op in & self . operations {
9695 match patch_op {
97- PatchOp :: Replace { value } => {
98- apply_group_replace_op ( value, & mut updated_group) ?
99- }
96+ PatchOp :: Replace { path, value } => apply_group_replace_op (
97+ path. as_ref ( ) ,
98+ value,
99+ & mut updated_group,
100+ ) ?,
100101 PatchOp :: Add { path, value } => apply_group_add_op (
101102 path. as_deref ( ) ,
102103 value,
@@ -113,31 +114,80 @@ impl PatchRequest {
113114}
114115
115116fn apply_group_replace_op (
117+ path : Option < & String > ,
116118 value : & serde_json:: Value ,
117119 group : & mut StoredParts < Group > ,
118120) -> Result < ( ) , PatchRequestError > {
119- #[ derive( Debug , Deserialize ) ]
120- #[ serde( rename_all = "camelCase" ) ]
121- struct DisplayName {
122- id : String ,
123- display_name : String ,
124- }
121+ match path {
122+ // Validate that we are replacing a groups members
123+ Some ( path) => {
124+ #[ derive( Debug , Deserialize ) ]
125+ #[ serde( rename_all = "camelCase" ) ]
126+ struct Member {
127+ value : String ,
128+ }
125129
126- let Ok ( change ) = serde_json :: from_value :: < DisplayName > ( value . clone ( ) )
127- else {
128- return Err ( PatchRequestError :: Unsupported (
129- "only replacing a groups displayName is supported" . to_string ( ) ,
130- ) ) ;
131- } ;
130+ if !path . eq_ignore_ascii_case ( "members" ) {
131+ return Err ( PatchRequestError :: Unsupported (
132+ "only replacing a groups members path is supported"
133+ . to_string ( ) ,
134+ ) ) ;
135+ }
132136
133- if !group. resource . id . eq_ignore_ascii_case ( & change. id ) {
134- return Err ( PatchRequestError :: Invalid ( format ! (
135- "unexpected group id {}" ,
136- change. id
137- ) ) ) ;
138- }
137+ let Ok ( patch_members) =
138+ serde_json:: from_value :: < Vec < Member > > ( value. clone ( ) )
139+ else {
140+ return Err ( PatchRequestError :: Invalid (
141+ "members being replaced in a group require a value field"
142+ . to_string ( ) ,
143+ ) ) ;
144+ } ;
139145
140- group. resource . display_name = change. display_name ;
146+ let new_members: IdOrdMap < GroupMember > = patch_members
147+ . into_iter ( )
148+ . map ( |member| GroupMember {
149+ resource_type : Some ( ResourceType :: User . to_string ( ) ) ,
150+ value : Some ( member. value ) ,
151+ } )
152+ . collect ( ) ;
153+ group. resource . members =
154+ ( !new_members. is_empty ( ) ) . then_some ( new_members) ;
155+ }
156+
157+ // RFC 7664 § 3.5.2.3:
158+ //
159+ // If the "path" parameter is omitted, the target is assumed to be the
160+ // resource itself. In this case, the "value" attribute SHALL contain a
161+ // list of one or more attributes that are to be replaced.
162+ None => {
163+ #[ derive( Debug , Deserialize ) ]
164+ #[ serde( rename_all = "camelCase" ) ]
165+ struct DisplayName {
166+ id : String ,
167+ display_name : String ,
168+ }
169+
170+ // We currently only support changing a display name
171+ let Ok ( change) =
172+ serde_json:: from_value :: < DisplayName > ( value. clone ( ) )
173+ else {
174+ return Err ( PatchRequestError :: Unsupported (
175+ "only replacing a displayName is supported when not
176+ including a path"
177+ . to_string ( ) ,
178+ ) ) ;
179+ } ;
180+
181+ if !group. resource . id . eq_ignore_ascii_case ( & change. id ) {
182+ return Err ( PatchRequestError :: Invalid ( format ! (
183+ "unexpected group id {}" ,
184+ change. id
185+ ) ) ) ;
186+ }
187+
188+ group. resource . display_name = change. display_name ;
189+ }
190+ }
141191
142192 Ok ( ( ) )
143193}
@@ -285,6 +335,29 @@ mod test {
285335 serde_json:: from_value :: < PatchRequest > ( json) . unwrap ( ) ;
286336 }
287337
338+ #[ test]
339+ fn test_parse_group_members_replace_op ( ) {
340+ let json = json ! ( {
341+ "schemas" : [
342+ "urn:ietf:params:scim:api:messages:2.0:PatchOp"
343+ ] ,
344+ "Operations" : [
345+ {
346+ "op" : "replace" ,
347+ "path" : "members" ,
348+ "value" : [
349+ {
350+ "value" : "abf4dd94-a4c0-4f67-89c9-76b03340cb9b" ,
351+ "display" : "dakota@example.com"
352+ }
353+ ]
354+ }
355+ ]
356+ } ) ;
357+
358+ serde_json:: from_value :: < PatchRequest > ( json) . unwrap ( ) ;
359+ }
360+
288361 #[ test]
289362 fn test_parse_group_membership_ops ( ) {
290363 let json = json ! ( {
0 commit comments