1use crate::prelude::*;
2use crate::schema::{SchemaAttribute, SchemaTransaction};
3use crate::server::batch_modify::{BatchModifyEvent, ModSetValid};
4use crate::server::ValueSetResolveStatus;
5use crate::valueset::*;
6use kanidm_proto::scim_v1::client::{ScimEntryPostGeneric, ScimEntryPutGeneric};
7use kanidm_proto::scim_v1::JsonValue;
8use std::collections::BTreeMap;
9
10#[derive(Debug)]
11pub struct ScimEntryPutEvent {
12 pub(crate) ident: Identity,
14
15 pub(crate) target: Uuid,
18 pub(crate) attrs: BTreeMap<Attribute, Option<ValueSet>>,
21
22 pub(crate) effective_access_check: bool,
25}
26
27impl ScimEntryPutEvent {
28 pub fn try_from(
29 ident: Identity,
30 entry: ScimEntryPutGeneric,
31 qs: &mut QueryServerWriteTransaction,
32 ) -> Result<Self, OperationError> {
33 let target = entry.id;
34
35 let attrs = entry
36 .attrs
37 .into_iter()
38 .map(|(attr, json_value)| {
39 qs.resolve_scim_json_put(&attr, json_value)
40 .map(|kani_value| (attr, kani_value))
41 })
42 .collect::<Result<_, _>>()?;
43
44 let query = entry.query;
45
46 Ok(ScimEntryPutEvent {
47 ident,
48 target,
49 attrs,
50 effective_access_check: query.ext_access_check,
51 })
52 }
53}
54
55#[derive(Debug)]
56pub struct ScimCreateEvent {
57 pub(crate) ident: Identity,
58 pub(crate) entry: EntryInitNew,
59}
60
61impl ScimCreateEvent {
62 pub fn try_from(
63 ident: Identity,
64 classes: &[EntryClass],
65 entry: ScimEntryPostGeneric,
66 qs: &mut QueryServerWriteTransaction,
67 ) -> Result<Self, OperationError> {
68 let mut entry = entry
69 .attrs
70 .into_iter()
71 .map(|(attr, json_value)| {
72 qs.resolve_scim_json_post(&attr, json_value)
73 .map(|kani_value| (attr, kani_value))
74 })
75 .collect::<Result<EntryInitNew, _>>()?;
76
77 if !classes.is_empty() {
78 let classes = ValueSetIutf8::from_iter(classes.iter().map(|cls| cls.as_ref()))
79 .ok_or(OperationError::SC0027ClassSetInvalid)?;
80
81 entry.set_ava_set(&Attribute::Class, classes);
82 }
83
84 Ok(ScimCreateEvent { ident, entry })
85 }
86}
87
88#[derive(Debug)]
89pub struct ScimDeleteEvent {
90 pub(crate) ident: Identity,
92
93 pub(crate) target: Uuid,
96
97 pub(crate) class: EntryClass,
99}
100
101impl ScimDeleteEvent {
102 pub fn new(ident: Identity, target: Uuid, class: EntryClass) -> Self {
103 ScimDeleteEvent {
104 ident,
105 target,
106 class,
107 }
108 }
109}
110
111impl QueryServerWriteTransaction<'_> {
112 pub fn scim_put(
117 &mut self,
118 scim_entry_put: ScimEntryPutEvent,
119 ) -> Result<ScimEntryKanidm, OperationError> {
120 let ScimEntryPutEvent {
121 ident,
122 target,
123 attrs,
124 effective_access_check,
125 } = scim_entry_put;
126
127 let mods_invalid: ModifyList<ModifyInvalid> = attrs.into();
129
130 let mods_valid = mods_invalid
131 .validate(self.get_schema())
132 .map_err(OperationError::SchemaViolation)?;
133
134 let mut modset = ModSetValid::default();
135
136 modset.insert(target, mods_valid);
137
138 let modify_event = BatchModifyEvent {
139 ident: ident.clone(),
140 modset,
141 };
142
143 self.batch_modify(&modify_event)?;
145
146 let filter_intent = filter!(f_and!([f_eq(Attribute::Uuid, PartialValue::Uuid(target))]));
149
150 let f_intent_valid = filter_intent
151 .validate(self.get_schema())
152 .map_err(OperationError::SchemaViolation)?;
153
154 let f_valid = f_intent_valid.clone().into_ignore_hidden();
155
156 let se = SearchEvent {
157 ident,
158 filter: f_valid,
159 filter_orig: f_intent_valid,
160 attrs: None,
162 effective_access_check,
163 };
164
165 let mut vs = self.search_ext(&se)?;
166 match vs.pop() {
167 Some(entry) if vs.is_empty() => entry.to_scim_kanidm(self),
168 _ => {
169 if vs.is_empty() {
170 Err(OperationError::NoMatchingEntries)
171 } else {
172 Err(OperationError::UniqueConstraintViolation)
174 }
175 }
176 }
177 }
178
179 pub fn scim_create(
180 &mut self,
181 scim_create: ScimCreateEvent,
182 ) -> Result<ScimEntryKanidm, OperationError> {
183 let ScimCreateEvent { ident, entry } = scim_create;
184
185 let create_event = CreateEvent {
186 ident,
187 entries: vec![entry],
188 return_created_uuids: true,
189 };
190
191 let changed_uuids = self.create(&create_event)?;
192
193 let mut changed_uuids = changed_uuids.ok_or(OperationError::SC0028CreatedUuidsInvalid)?;
194
195 let target = if let Some(target) = changed_uuids.pop() {
196 if !changed_uuids.is_empty() {
197 return Err(OperationError::UniqueConstraintViolation);
199 }
200
201 target
202 } else {
203 return Err(OperationError::NoMatchingEntries);
205 };
206
207 let filter_intent = filter!(f_and!([f_eq(Attribute::Uuid, PartialValue::Uuid(target))]));
210
211 let f_intent_valid = filter_intent
212 .validate(self.get_schema())
213 .map_err(OperationError::SchemaViolation)?;
214
215 let f_valid = f_intent_valid.clone().into_ignore_hidden();
216
217 let se = SearchEvent {
218 ident: create_event.ident,
219 filter: f_valid,
220 filter_orig: f_intent_valid,
221 attrs: None,
223 effective_access_check: false,
224 };
225
226 let mut vs = self.search_ext(&se)?;
227 match vs.pop() {
228 Some(entry) if vs.is_empty() => entry.to_scim_kanidm(self),
229 _ => {
230 if vs.is_empty() {
231 Err(OperationError::NoMatchingEntries)
232 } else {
233 Err(OperationError::UniqueConstraintViolation)
235 }
236 }
237 }
238 }
239
240 pub fn scim_delete(&mut self, scim_delete: ScimDeleteEvent) -> Result<(), OperationError> {
241 let ScimDeleteEvent {
242 ident,
243 target,
244 class,
245 } = scim_delete;
246
247 let filter_intent = filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(target)));
248 let f_intent_valid = filter_intent
249 .validate(self.get_schema())
250 .map_err(OperationError::SchemaViolation)?;
251
252 let filter = filter!(f_and!([
253 f_eq(Attribute::Uuid, PartialValue::Uuid(target)),
254 f_eq(Attribute::Class, class.into())
255 ]));
256 let f_valid = filter
257 .validate(self.get_schema())
258 .map_err(OperationError::SchemaViolation)?;
259
260 let de = DeleteEvent {
261 ident,
262 filter: f_valid,
263 filter_orig: f_intent_valid,
264 };
265
266 self.delete(&de)
267 }
268
269 pub(crate) fn resolve_scim_json_put(
270 &mut self,
271 attr: &Attribute,
272 value: Option<JsonValue>,
273 ) -> Result<Option<ValueSet>, OperationError> {
274 let schema = self.get_schema();
275 let Some(schema_a) = schema.get_attributes().get(attr) else {
277 return Err(OperationError::InvalidAttributeName(attr.to_string()));
280 };
281
282 let Some(value) = value else {
283 return Ok(None);
286 };
287
288 self.resolve_scim_json(schema_a, value).map(Some)
289 }
290
291 pub(crate) fn resolve_scim_json_post(
292 &mut self,
293 attr: &Attribute,
294 value: JsonValue,
295 ) -> Result<ValueSet, OperationError> {
296 let schema = self.get_schema();
297 let Some(schema_a) = schema.get_attributes().get(attr) else {
299 return Err(OperationError::InvalidAttributeName(attr.to_string()));
302 };
303
304 self.resolve_scim_json(schema_a, value)
305 }
306
307 fn resolve_scim_json(
308 &mut self,
309 schema_a: &SchemaAttribute,
310 value: JsonValue,
311 ) -> Result<ValueSet, OperationError> {
312 let resolve_status = match schema_a.syntax {
313 SyntaxType::Utf8String => ValueSetUtf8::from_scim_json_put(value),
314 SyntaxType::Utf8StringInsensitive => ValueSetIutf8::from_scim_json_put(value),
315 SyntaxType::Uuid => ValueSetUuid::from_scim_json_put(value),
316 SyntaxType::Boolean => ValueSetBool::from_scim_json_put(value),
317 SyntaxType::SyntaxId => ValueSetSyntax::from_scim_json_put(value),
318 SyntaxType::IndexId => ValueSetIndex::from_scim_json_put(value),
319 SyntaxType::ReferenceUuid => ValueSetRefer::from_scim_json_put(value),
320 SyntaxType::Utf8StringIname => ValueSetIname::from_scim_json_put(value),
321 SyntaxType::NsUniqueId => ValueSetNsUniqueId::from_scim_json_put(value),
322 SyntaxType::DateTime => ValueSetDateTime::from_scim_json_put(value),
323 SyntaxType::EmailAddress => ValueSetEmailAddress::from_scim_json_put(value),
324 SyntaxType::Url => ValueSetUrl::from_scim_json_put(value),
325 SyntaxType::OauthScope => ValueSetOauthScope::from_scim_json_put(value),
326 SyntaxType::OauthScopeMap => ValueSetOauthScopeMap::from_scim_json_put(value),
327 SyntaxType::OauthClaimMap => ValueSetOauthClaimMap::from_scim_json_put(value),
328 SyntaxType::UiHint => ValueSetUiHint::from_scim_json_put(value),
329 SyntaxType::CredentialType => ValueSetCredentialType::from_scim_json_put(value),
330 SyntaxType::Certificate => ValueSetCertificate::from_scim_json_put(value),
331 SyntaxType::SshKey => ValueSetSshKey::from_scim_json_put(value),
332 SyntaxType::Uint32 => ValueSetUint32::from_scim_json_put(value),
333 SyntaxType::Int64 => ValueSetInt64::from_scim_json_put(value),
334 SyntaxType::Uint64 => ValueSetUint64::from_scim_json_put(value),
335 SyntaxType::Sha256 => ValueSetSha256::from_scim_json_put(value),
336
337 SyntaxType::JsonFilter => Err(OperationError::InvalidAttribute(
340 "Json Filters are not able to be set.".to_string(),
341 )),
342 SyntaxType::Json => Err(OperationError::InvalidAttribute(
344 "Json values are not able to be set.".to_string(),
345 )),
346 SyntaxType::Message => Err(OperationError::InvalidAttribute(
347 "Message values are not able to be set.".to_string(),
348 )),
349 SyntaxType::HexString => Err(OperationError::InvalidAttribute(
352 "Hex strings are not able to be set.".to_string(),
353 )),
354
355 SyntaxType::Image => Err(OperationError::InvalidAttribute(
358 "Images are not able to be set.".to_string(),
359 )),
360
361 SyntaxType::WebauthnAttestationCaList => Err(OperationError::InvalidAttribute(
366 "Webauthn Attestation Ca Lists are not able to be set.".to_string(),
367 )),
368
369 SyntaxType::Credential => Err(OperationError::InvalidAttribute(
371 "Credentials are not able to be set.".to_string(),
372 )),
373 SyntaxType::SecretUtf8String => Err(OperationError::InvalidAttribute(
374 "Secrets are not able to be set.".to_string(),
375 )),
376 SyntaxType::SecurityPrincipalName => Err(OperationError::InvalidAttribute(
377 "SPNs are not able to be set.".to_string(),
378 )),
379 SyntaxType::Cid => Err(OperationError::InvalidAttribute(
380 "CIDs are not able to be set.".to_string(),
381 )),
382 SyntaxType::PrivateBinary => Err(OperationError::InvalidAttribute(
383 "Private Binaries are not able to be set.".to_string(),
384 )),
385 SyntaxType::IntentToken => Err(OperationError::InvalidAttribute(
386 "Intent Tokens are not able to be set.".to_string(),
387 )),
388 SyntaxType::Passkey => Err(OperationError::InvalidAttribute(
389 "Passkeys are not able to be set.".to_string(),
390 )),
391 SyntaxType::AttestedPasskey => Err(OperationError::InvalidAttribute(
392 "Attested Passkeys are not able to be set.".to_string(),
393 )),
394 SyntaxType::Session => Err(OperationError::InvalidAttribute(
395 "Sessions are not able to be set.".to_string(),
396 )),
397 SyntaxType::JwsKeyEs256 => Err(OperationError::InvalidAttribute(
398 "Jws ES256 Private Keys are not able to be set.".to_string(),
399 )),
400 SyntaxType::JwsKeyRs256 => Err(OperationError::InvalidAttribute(
401 "Jws RS256 Private Keys are not able to be set.".to_string(),
402 )),
403 SyntaxType::Oauth2Session => Err(OperationError::InvalidAttribute(
404 "Sessions are not able to be set.".to_string(),
405 )),
406 SyntaxType::TotpSecret => Err(OperationError::InvalidAttribute(
407 "TOTP Secrets are not able to be set.".to_string(),
408 )),
409 SyntaxType::ApiToken => Err(OperationError::InvalidAttribute(
410 "API Tokens are not able to be set.".to_string(),
411 )),
412 SyntaxType::AuditLogString => Err(OperationError::InvalidAttribute(
413 "Audit Strings are not able to be set.".to_string(),
414 )),
415 SyntaxType::EcKeyPrivate => Err(OperationError::InvalidAttribute(
416 "EC Private Keys are not able to be set.".to_string(),
417 )),
418 SyntaxType::KeyInternal => Err(OperationError::InvalidAttribute(
419 "Key Internal Structures are not able to be set.".to_string(),
420 )),
421 SyntaxType::ApplicationPassword => Err(OperationError::InvalidAttribute(
422 "Application Passwords are not able to be set.".to_string(),
423 )),
424 }?;
425
426 match resolve_status {
427 ValueSetResolveStatus::Resolved(vs) => Ok(vs),
428 ValueSetResolveStatus::NeedsResolution(vs_inter) => {
429 self.resolve_valueset_intermediate(vs_inter)
430 }
431 }
432 }
433}
434
435#[cfg(test)]
436mod tests {
437 use super::ScimEntryPutEvent;
438 use crate::prelude::*;
439 use kanidm_proto::scim_v1::client::ScimEntryPutKanidm;
440 use kanidm_proto::scim_v1::server::ScimReference;
441 use kanidm_proto::scim_v1::ScimMail;
442
443 #[qs_test]
444 async fn scim_put_basic(server: &QueryServer) {
445 let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
446
447 let idm_admin_entry = server_txn.internal_search_uuid(UUID_IDM_ADMIN).unwrap();
448
449 let idm_admin_ident = Identity::from_impersonate_entry_readwrite(idm_admin_entry);
450
451 let group_uuid = Uuid::new_v4();
453
454 let extra1_uuid = Uuid::new_v4();
456 let extra2_uuid = Uuid::new_v4();
457 let extra3_uuid = Uuid::new_v4();
458
459 let e1 = entry_init!(
460 (Attribute::Class, EntryClass::Object.to_value()),
461 (Attribute::Class, EntryClass::Group.to_value()),
462 (Attribute::Name, Value::new_iname("testgroup")),
463 (Attribute::Uuid, Value::Uuid(group_uuid))
464 );
465
466 let e2 = entry_init!(
467 (Attribute::Class, EntryClass::Object.to_value()),
468 (Attribute::Class, EntryClass::Group.to_value()),
469 (Attribute::Name, Value::new_iname("extra_1")),
470 (Attribute::Uuid, Value::Uuid(extra1_uuid))
471 );
472
473 let e3 = entry_init!(
474 (Attribute::Class, EntryClass::Object.to_value()),
475 (Attribute::Class, EntryClass::Group.to_value()),
476 (Attribute::Name, Value::new_iname("extra_2")),
477 (Attribute::Uuid, Value::Uuid(extra2_uuid))
478 );
479
480 let e4 = entry_init!(
481 (Attribute::Class, EntryClass::Object.to_value()),
482 (Attribute::Class, EntryClass::Group.to_value()),
483 (Attribute::Name, Value::new_iname("extra_3")),
484 (Attribute::Uuid, Value::Uuid(extra3_uuid))
485 );
486
487 assert!(server_txn.internal_create(vec![e1, e2, e3, e4]).is_ok());
488
489 let test_mails = vec![
491 ScimMail {
492 primary: true,
493 value: "test@test.test".to_string(),
494 },
495 ScimMail {
496 primary: false,
497 value: "test2@test.test".to_string(),
498 },
499 ];
500 let put = ScimEntryPutKanidm {
501 id: group_uuid,
502 attrs: [
503 (Attribute::Description, Some("Group Description".into())),
504 (
505 Attribute::Mail,
506 Some(ScimValueKanidm::Mail(test_mails.clone())),
507 ),
508 ]
509 .into(),
510 };
511
512 let put_generic = put.try_into().unwrap();
513 let put_event =
514 ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
515 .expect("Failed to resolve data type");
516
517 let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
518 let desc = updated_entry.attrs.get(&Attribute::Description).unwrap();
519 let mails = updated_entry.attrs.get(&Attribute::Mail).unwrap();
520
521 match desc {
522 ScimValueKanidm::String(gdesc) if gdesc == "Group Description" => {}
523 _ => unreachable!("Expected a string"),
524 };
525
526 let ScimValueKanidm::Mail(mails) = mails else {
527 unreachable!("Expected an email")
528 };
529
530 assert!(mails.iter().all(|mail| test_mails.contains(mail)));
532
533 let put = ScimEntryPutKanidm {
535 id: group_uuid,
536 attrs: [(Attribute::Description, None)].into(),
537 };
538
539 let put_generic = put.try_into().unwrap();
540 let put_event =
541 ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
542 .expect("Failed to resolve data type");
543
544 let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
545 assert!(!updated_entry.attrs.contains_key(&Attribute::Description));
546
547 let put = ScimEntryPutKanidm {
549 id: group_uuid,
550 attrs: [(
551 Attribute::Member,
552 Some(ScimValueKanidm::EntryReferences(vec![ScimReference {
553 uuid: extra1_uuid,
554 value: String::default(),
556 }])),
557 )]
558 .into(),
559 };
560
561 let put_generic = put.try_into().unwrap();
562 let put_event =
563 ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
564 .expect("Failed to resolve data type");
565
566 let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
567 let members = updated_entry.attrs.get(&Attribute::Member).unwrap();
568
569 trace!(?members);
570
571 match members {
572 ScimValueKanidm::EntryReferences(member_set) if member_set.len() == 1 => {
573 assert!(member_set.contains(&ScimReference {
574 uuid: extra1_uuid,
575 value: "extra_1@example.com".to_string(),
576 }));
577 }
578 _ => unreachable!("Expected 1 member"),
579 };
580
581 let put = ScimEntryPutKanidm {
583 id: group_uuid,
584 attrs: [(
585 Attribute::Member,
586 Some(ScimValueKanidm::EntryReferences(vec![
587 ScimReference {
588 uuid: extra1_uuid,
589 value: String::default(),
590 },
591 ScimReference {
592 uuid: extra2_uuid,
593 value: String::default(),
594 },
595 ScimReference {
596 uuid: extra3_uuid,
597 value: String::default(),
598 },
599 ])),
600 )]
601 .into(),
602 };
603
604 let put_generic = put.try_into().unwrap();
605 let put_event =
606 ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
607 .expect("Failed to resolve data type");
608
609 let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
610 let members = updated_entry.attrs.get(&Attribute::Member).unwrap();
611
612 trace!(?members);
613
614 match members {
615 ScimValueKanidm::EntryReferences(member_set) if member_set.len() == 3 => {
616 assert!(member_set.contains(&ScimReference {
617 uuid: extra1_uuid,
618 value: "extra_1@example.com".to_string(),
619 }));
620 assert!(member_set.contains(&ScimReference {
621 uuid: extra2_uuid,
622 value: "extra_2@example.com".to_string(),
623 }));
624 assert!(member_set.contains(&ScimReference {
625 uuid: extra3_uuid,
626 value: "extra_3@example.com".to_string(),
627 }));
628 }
629 _ => unreachable!("Expected 3 members"),
630 };
631
632 let put = ScimEntryPutKanidm {
634 id: group_uuid,
635 attrs: [(
636 Attribute::Member,
637 Some(ScimValueKanidm::EntryReferences(vec![
638 ScimReference {
639 uuid: extra1_uuid,
640 value: String::default(),
641 },
642 ScimReference {
643 uuid: extra3_uuid,
644 value: String::default(),
645 },
646 ])),
647 )]
648 .into(),
649 };
650
651 let put_generic = put.try_into().unwrap();
652 let put_event =
653 ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
654 .expect("Failed to resolve data type");
655
656 let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
657 let members = updated_entry.attrs.get(&Attribute::Member).unwrap();
658
659 trace!(?members);
660
661 match members {
662 ScimValueKanidm::EntryReferences(member_set) if member_set.len() == 2 => {
663 assert!(member_set.contains(&ScimReference {
664 uuid: extra1_uuid,
665 value: "extra_1@example.com".to_string(),
666 }));
667 assert!(member_set.contains(&ScimReference {
668 uuid: extra3_uuid,
669 value: "extra_3@example.com".to_string(),
670 }));
671 assert!(!member_set.contains(&ScimReference {
673 uuid: extra2_uuid,
674 value: "extra_2@example.com".to_string(),
675 }));
676 }
677 _ => unreachable!("Expected 2 members"),
678 };
679
680 let put = ScimEntryPutKanidm {
682 id: group_uuid,
683 attrs: [(Attribute::Member, None)].into(),
684 };
685
686 let put_generic = put.try_into().unwrap();
687 let put_event =
688 ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
689 .expect("Failed to resolve data type");
690
691 let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
692 assert!(!updated_entry.attrs.contains_key(&Attribute::Member));
693 }
694}