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