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