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::HexString => Err(OperationError::InvalidAttribute(
340 "Hex strings are not able to be set.".to_string(),
341 )),
342
343 SyntaxType::Image => Err(OperationError::InvalidAttribute(
346 "Images are not able to be set.".to_string(),
347 )),
348
349 SyntaxType::WebauthnAttestationCaList => Err(OperationError::InvalidAttribute(
354 "Webauthn Attestation Ca Lists are not able to be set.".to_string(),
355 )),
356
357 SyntaxType::Credential => Err(OperationError::InvalidAttribute(
359 "Credentials are not able to be set.".to_string(),
360 )),
361 SyntaxType::SecretUtf8String => Err(OperationError::InvalidAttribute(
362 "Secrets are not able to be set.".to_string(),
363 )),
364 SyntaxType::SecurityPrincipalName => Err(OperationError::InvalidAttribute(
365 "SPNs are not able to be set.".to_string(),
366 )),
367 SyntaxType::Cid => Err(OperationError::InvalidAttribute(
368 "CIDs are not able to be set.".to_string(),
369 )),
370 SyntaxType::PrivateBinary => Err(OperationError::InvalidAttribute(
371 "Private Binaries are not able to be set.".to_string(),
372 )),
373 SyntaxType::IntentToken => Err(OperationError::InvalidAttribute(
374 "Intent Tokens are not able to be set.".to_string(),
375 )),
376 SyntaxType::Passkey => Err(OperationError::InvalidAttribute(
377 "Passkeys are not able to be set.".to_string(),
378 )),
379 SyntaxType::AttestedPasskey => Err(OperationError::InvalidAttribute(
380 "Attested Passkeys are not able to be set.".to_string(),
381 )),
382 SyntaxType::Session => Err(OperationError::InvalidAttribute(
383 "Sessions are not able to be set.".to_string(),
384 )),
385 SyntaxType::JwsKeyEs256 => Err(OperationError::InvalidAttribute(
386 "Jws ES256 Private Keys are not able to be set.".to_string(),
387 )),
388 SyntaxType::JwsKeyRs256 => Err(OperationError::InvalidAttribute(
389 "Jws RS256 Private Keys are not able to be set.".to_string(),
390 )),
391 SyntaxType::Oauth2Session => Err(OperationError::InvalidAttribute(
392 "Sessions are not able to be set.".to_string(),
393 )),
394 SyntaxType::TotpSecret => Err(OperationError::InvalidAttribute(
395 "TOTP Secrets are not able to be set.".to_string(),
396 )),
397 SyntaxType::ApiToken => Err(OperationError::InvalidAttribute(
398 "API Tokens are not able to be set.".to_string(),
399 )),
400 SyntaxType::AuditLogString => Err(OperationError::InvalidAttribute(
401 "Audit Strings are not able to be set.".to_string(),
402 )),
403 SyntaxType::EcKeyPrivate => Err(OperationError::InvalidAttribute(
404 "EC Private Keys are not able to be set.".to_string(),
405 )),
406 SyntaxType::KeyInternal => Err(OperationError::InvalidAttribute(
407 "Key Internal Structures are not able to be set.".to_string(),
408 )),
409 SyntaxType::ApplicationPassword => Err(OperationError::InvalidAttribute(
410 "Application Passwords are not able to be set.".to_string(),
411 )),
412 }?;
413
414 match resolve_status {
415 ValueSetResolveStatus::Resolved(vs) => Ok(vs),
416 ValueSetResolveStatus::NeedsResolution(vs_inter) => {
417 self.resolve_valueset_intermediate(vs_inter)
418 }
419 }
420 }
421}
422
423#[cfg(test)]
424mod tests {
425 use super::ScimEntryPutEvent;
426 use crate::prelude::*;
427 use kanidm_proto::scim_v1::client::ScimEntryPutKanidm;
428 use kanidm_proto::scim_v1::server::ScimReference;
429 use kanidm_proto::scim_v1::ScimMail;
430
431 #[qs_test]
432 async fn scim_put_basic(server: &QueryServer) {
433 let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
434
435 let idm_admin_entry = server_txn.internal_search_uuid(UUID_IDM_ADMIN).unwrap();
436
437 let idm_admin_ident = Identity::from_impersonate_entry_readwrite(idm_admin_entry);
438
439 let group_uuid = Uuid::new_v4();
441
442 let extra1_uuid = Uuid::new_v4();
444 let extra2_uuid = Uuid::new_v4();
445 let extra3_uuid = Uuid::new_v4();
446
447 let e1 = entry_init!(
448 (Attribute::Class, EntryClass::Object.to_value()),
449 (Attribute::Class, EntryClass::Group.to_value()),
450 (Attribute::Name, Value::new_iname("testgroup")),
451 (Attribute::Uuid, Value::Uuid(group_uuid))
452 );
453
454 let e2 = entry_init!(
455 (Attribute::Class, EntryClass::Object.to_value()),
456 (Attribute::Class, EntryClass::Group.to_value()),
457 (Attribute::Name, Value::new_iname("extra_1")),
458 (Attribute::Uuid, Value::Uuid(extra1_uuid))
459 );
460
461 let e3 = entry_init!(
462 (Attribute::Class, EntryClass::Object.to_value()),
463 (Attribute::Class, EntryClass::Group.to_value()),
464 (Attribute::Name, Value::new_iname("extra_2")),
465 (Attribute::Uuid, Value::Uuid(extra2_uuid))
466 );
467
468 let e4 = entry_init!(
469 (Attribute::Class, EntryClass::Object.to_value()),
470 (Attribute::Class, EntryClass::Group.to_value()),
471 (Attribute::Name, Value::new_iname("extra_3")),
472 (Attribute::Uuid, Value::Uuid(extra3_uuid))
473 );
474
475 assert!(server_txn.internal_create(vec![e1, e2, e3, e4]).is_ok());
476
477 let test_mails = vec![
479 ScimMail {
480 primary: true,
481 value: "test@test.test".to_string(),
482 },
483 ScimMail {
484 primary: false,
485 value: "test2@test.test".to_string(),
486 },
487 ];
488 let put = ScimEntryPutKanidm {
489 id: group_uuid,
490 attrs: [
491 (Attribute::Description, Some("Group Description".into())),
492 (
493 Attribute::Mail,
494 Some(ScimValueKanidm::Mail(test_mails.clone())),
495 ),
496 ]
497 .into(),
498 };
499
500 let put_generic = put.try_into().unwrap();
501 let put_event =
502 ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
503 .expect("Failed to resolve data type");
504
505 let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
506 let desc = updated_entry.attrs.get(&Attribute::Description).unwrap();
507 let mails = updated_entry.attrs.get(&Attribute::Mail).unwrap();
508
509 match desc {
510 ScimValueKanidm::String(gdesc) if gdesc == "Group Description" => {}
511 _ => unreachable!("Expected a string"),
512 };
513
514 let ScimValueKanidm::Mail(mails) = mails else {
515 unreachable!("Expected an email")
516 };
517
518 assert!(mails.iter().all(|mail| test_mails.contains(mail)));
520
521 let put = ScimEntryPutKanidm {
523 id: group_uuid,
524 attrs: [(Attribute::Description, None)].into(),
525 };
526
527 let put_generic = put.try_into().unwrap();
528 let put_event =
529 ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
530 .expect("Failed to resolve data type");
531
532 let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
533 assert!(!updated_entry.attrs.contains_key(&Attribute::Description));
534
535 let put = ScimEntryPutKanidm {
537 id: group_uuid,
538 attrs: [(
539 Attribute::Member,
540 Some(ScimValueKanidm::EntryReferences(vec![ScimReference {
541 uuid: extra1_uuid,
542 value: String::default(),
544 }])),
545 )]
546 .into(),
547 };
548
549 let put_generic = put.try_into().unwrap();
550 let put_event =
551 ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
552 .expect("Failed to resolve data type");
553
554 let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
555 let members = updated_entry.attrs.get(&Attribute::Member).unwrap();
556
557 trace!(?members);
558
559 match members {
560 ScimValueKanidm::EntryReferences(member_set) if member_set.len() == 1 => {
561 assert!(member_set.contains(&ScimReference {
562 uuid: extra1_uuid,
563 value: "extra_1@example.com".to_string(),
564 }));
565 }
566 _ => unreachable!("Expected 1 member"),
567 };
568
569 let put = ScimEntryPutKanidm {
571 id: group_uuid,
572 attrs: [(
573 Attribute::Member,
574 Some(ScimValueKanidm::EntryReferences(vec![
575 ScimReference {
576 uuid: extra1_uuid,
577 value: String::default(),
578 },
579 ScimReference {
580 uuid: extra2_uuid,
581 value: String::default(),
582 },
583 ScimReference {
584 uuid: extra3_uuid,
585 value: String::default(),
586 },
587 ])),
588 )]
589 .into(),
590 };
591
592 let put_generic = put.try_into().unwrap();
593 let put_event =
594 ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
595 .expect("Failed to resolve data type");
596
597 let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
598 let members = updated_entry.attrs.get(&Attribute::Member).unwrap();
599
600 trace!(?members);
601
602 match members {
603 ScimValueKanidm::EntryReferences(member_set) if member_set.len() == 3 => {
604 assert!(member_set.contains(&ScimReference {
605 uuid: extra1_uuid,
606 value: "extra_1@example.com".to_string(),
607 }));
608 assert!(member_set.contains(&ScimReference {
609 uuid: extra2_uuid,
610 value: "extra_2@example.com".to_string(),
611 }));
612 assert!(member_set.contains(&ScimReference {
613 uuid: extra3_uuid,
614 value: "extra_3@example.com".to_string(),
615 }));
616 }
617 _ => unreachable!("Expected 3 members"),
618 };
619
620 let put = ScimEntryPutKanidm {
622 id: group_uuid,
623 attrs: [(
624 Attribute::Member,
625 Some(ScimValueKanidm::EntryReferences(vec![
626 ScimReference {
627 uuid: extra1_uuid,
628 value: String::default(),
629 },
630 ScimReference {
631 uuid: extra3_uuid,
632 value: String::default(),
633 },
634 ])),
635 )]
636 .into(),
637 };
638
639 let put_generic = put.try_into().unwrap();
640 let put_event =
641 ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
642 .expect("Failed to resolve data type");
643
644 let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
645 let members = updated_entry.attrs.get(&Attribute::Member).unwrap();
646
647 trace!(?members);
648
649 match members {
650 ScimValueKanidm::EntryReferences(member_set) if member_set.len() == 2 => {
651 assert!(member_set.contains(&ScimReference {
652 uuid: extra1_uuid,
653 value: "extra_1@example.com".to_string(),
654 }));
655 assert!(member_set.contains(&ScimReference {
656 uuid: extra3_uuid,
657 value: "extra_3@example.com".to_string(),
658 }));
659 assert!(!member_set.contains(&ScimReference {
661 uuid: extra2_uuid,
662 value: "extra_2@example.com".to_string(),
663 }));
664 }
665 _ => unreachable!("Expected 2 members"),
666 };
667
668 let put = ScimEntryPutKanidm {
670 id: group_uuid,
671 attrs: [(Attribute::Member, None)].into(),
672 };
673
674 let put_generic = put.try_into().unwrap();
675 let put_event =
676 ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
677 .expect("Failed to resolve data type");
678
679 let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
680 assert!(!updated_entry.attrs.contains_key(&Attribute::Member));
681 }
682}