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
430 #[qs_test]
431 async fn scim_put_basic(server: &QueryServer) {
432 let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
433
434 let idm_admin_entry = server_txn.internal_search_uuid(UUID_IDM_ADMIN).unwrap();
435
436 let idm_admin_ident = Identity::from_impersonate_entry_readwrite(idm_admin_entry);
437
438 let group_uuid = Uuid::new_v4();
440
441 let extra1_uuid = Uuid::new_v4();
443 let extra2_uuid = Uuid::new_v4();
444 let extra3_uuid = Uuid::new_v4();
445
446 let e1 = entry_init!(
447 (Attribute::Class, EntryClass::Object.to_value()),
448 (Attribute::Class, EntryClass::Group.to_value()),
449 (Attribute::Name, Value::new_iname("testgroup")),
450 (Attribute::Uuid, Value::Uuid(group_uuid))
451 );
452
453 let e2 = entry_init!(
454 (Attribute::Class, EntryClass::Object.to_value()),
455 (Attribute::Class, EntryClass::Group.to_value()),
456 (Attribute::Name, Value::new_iname("extra_1")),
457 (Attribute::Uuid, Value::Uuid(extra1_uuid))
458 );
459
460 let e3 = entry_init!(
461 (Attribute::Class, EntryClass::Object.to_value()),
462 (Attribute::Class, EntryClass::Group.to_value()),
463 (Attribute::Name, Value::new_iname("extra_2")),
464 (Attribute::Uuid, Value::Uuid(extra2_uuid))
465 );
466
467 let e4 = entry_init!(
468 (Attribute::Class, EntryClass::Object.to_value()),
469 (Attribute::Class, EntryClass::Group.to_value()),
470 (Attribute::Name, Value::new_iname("extra_3")),
471 (Attribute::Uuid, Value::Uuid(extra3_uuid))
472 );
473
474 assert!(server_txn.internal_create(vec![e1, e2, e3, e4]).is_ok());
475
476 let put = ScimEntryPutKanidm {
478 id: group_uuid,
479 attrs: [(Attribute::Description, Some("Group Description".into()))].into(),
480 };
481
482 let put_generic = put.try_into().unwrap();
483 let put_event =
484 ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
485 .expect("Failed to resolve data type");
486
487 let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
488 let desc = updated_entry.attrs.get(&Attribute::Description).unwrap();
489
490 match desc {
491 ScimValueKanidm::String(gdesc) if gdesc == "Group Description" => {}
492 _ => unreachable!("Expected a string"),
493 };
494
495 let put = ScimEntryPutKanidm {
497 id: group_uuid,
498 attrs: [(Attribute::Description, None)].into(),
499 };
500
501 let put_generic = put.try_into().unwrap();
502 let put_event =
503 ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
504 .expect("Failed to resolve data type");
505
506 let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
507 assert!(!updated_entry.attrs.contains_key(&Attribute::Description));
508
509 let put = ScimEntryPutKanidm {
511 id: group_uuid,
512 attrs: [(
513 Attribute::Member,
514 Some(ScimValueKanidm::EntryReferences(vec![ScimReference {
515 uuid: extra1_uuid,
516 value: String::default(),
518 }])),
519 )]
520 .into(),
521 };
522
523 let put_generic = put.try_into().unwrap();
524 let put_event =
525 ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
526 .expect("Failed to resolve data type");
527
528 let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
529 let members = updated_entry.attrs.get(&Attribute::Member).unwrap();
530
531 trace!(?members);
532
533 match members {
534 ScimValueKanidm::EntryReferences(member_set) if member_set.len() == 1 => {
535 assert!(member_set.contains(&ScimReference {
536 uuid: extra1_uuid,
537 value: "extra_1@example.com".to_string(),
538 }));
539 }
540 _ => unreachable!("Expected 1 member"),
541 };
542
543 let put = ScimEntryPutKanidm {
545 id: group_uuid,
546 attrs: [(
547 Attribute::Member,
548 Some(ScimValueKanidm::EntryReferences(vec![
549 ScimReference {
550 uuid: extra1_uuid,
551 value: String::default(),
552 },
553 ScimReference {
554 uuid: extra2_uuid,
555 value: String::default(),
556 },
557 ScimReference {
558 uuid: extra3_uuid,
559 value: String::default(),
560 },
561 ])),
562 )]
563 .into(),
564 };
565
566 let put_generic = put.try_into().unwrap();
567 let put_event =
568 ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
569 .expect("Failed to resolve data type");
570
571 let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
572 let members = updated_entry.attrs.get(&Attribute::Member).unwrap();
573
574 trace!(?members);
575
576 match members {
577 ScimValueKanidm::EntryReferences(member_set) if member_set.len() == 3 => {
578 assert!(member_set.contains(&ScimReference {
579 uuid: extra1_uuid,
580 value: "extra_1@example.com".to_string(),
581 }));
582 assert!(member_set.contains(&ScimReference {
583 uuid: extra2_uuid,
584 value: "extra_2@example.com".to_string(),
585 }));
586 assert!(member_set.contains(&ScimReference {
587 uuid: extra3_uuid,
588 value: "extra_3@example.com".to_string(),
589 }));
590 }
591 _ => unreachable!("Expected 3 members"),
592 };
593
594 let put = ScimEntryPutKanidm {
596 id: group_uuid,
597 attrs: [(
598 Attribute::Member,
599 Some(ScimValueKanidm::EntryReferences(vec![
600 ScimReference {
601 uuid: extra1_uuid,
602 value: String::default(),
603 },
604 ScimReference {
605 uuid: extra3_uuid,
606 value: String::default(),
607 },
608 ])),
609 )]
610 .into(),
611 };
612
613 let put_generic = put.try_into().unwrap();
614 let put_event =
615 ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
616 .expect("Failed to resolve data type");
617
618 let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
619 let members = updated_entry.attrs.get(&Attribute::Member).unwrap();
620
621 trace!(?members);
622
623 match members {
624 ScimValueKanidm::EntryReferences(member_set) if member_set.len() == 2 => {
625 assert!(member_set.contains(&ScimReference {
626 uuid: extra1_uuid,
627 value: "extra_1@example.com".to_string(),
628 }));
629 assert!(member_set.contains(&ScimReference {
630 uuid: extra3_uuid,
631 value: "extra_3@example.com".to_string(),
632 }));
633 assert!(!member_set.contains(&ScimReference {
635 uuid: extra2_uuid,
636 value: "extra_2@example.com".to_string(),
637 }));
638 }
639 _ => unreachable!("Expected 2 members"),
640 };
641
642 let put = ScimEntryPutKanidm {
644 id: group_uuid,
645 attrs: [(Attribute::Member, None)].into(),
646 };
647
648 let put_generic = put.try_into().unwrap();
649 let put_event =
650 ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
651 .expect("Failed to resolve data type");
652
653 let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
654 assert!(!updated_entry.attrs.contains_key(&Attribute::Member));
655 }
656}