kanidmd_lib/valueset/
oauth.rs

1use crate::valueset::ScimResolveStatus;
2use std::collections::btree_map::Entry as BTreeEntry;
3use std::collections::{BTreeMap, BTreeSet};
4
5use crate::be::dbvalue::{DbValueOauthClaimMap, DbValueOauthScopeMapV1};
6use crate::prelude::*;
7use crate::schema::SchemaAttribute;
8use crate::value::{OauthClaimMapJoin, OAUTHSCOPE_RE};
9use crate::valueset::{
10    uuid_to_proto_string, DbValueSetV2, ResolvedValueSetOauth2ClaimMap,
11    ResolvedValueSetOauth2ScopeMap, ScimValueIntermediate, UnresolvedScimValueOauth2ClaimMap,
12    UnresolvedScimValueOauth2ScopeMap, UnresolvedValueSetOauth2ClaimMap,
13    UnresolvedValueSetOauth2ScopeMap, ValueSet, ValueSetIntermediate, ValueSetResolveStatus,
14    ValueSetScimPut,
15};
16use kanidm_proto::scim_v1::client::ScimOAuth2ClaimMap as ClientScimOAuth2ClaimMap;
17use kanidm_proto::scim_v1::client::ScimOAuth2ScopeMap as ClientScimOAuth2ScopeMap;
18use kanidm_proto::scim_v1::JsonValue;
19
20#[derive(Debug, Clone)]
21pub struct ValueSetOauthScope {
22    set: BTreeSet<String>,
23}
24
25impl ValueSetOauthScope {
26    pub fn new(s: String) -> Box<Self> {
27        let mut set = BTreeSet::new();
28        set.insert(s);
29        Box::new(ValueSetOauthScope { set })
30    }
31
32    pub fn push(&mut self, s: String) -> bool {
33        self.set.insert(s)
34    }
35
36    pub fn from_dbvs2(data: Vec<String>) -> Result<ValueSet, OperationError> {
37        let set = data.into_iter().collect();
38        Ok(Box::new(ValueSetOauthScope { set }))
39    }
40
41    // We need to allow this, because rust doesn't allow us to impl FromIterator on foreign
42    // types, and String is foreign.
43    #[allow(clippy::should_implement_trait)]
44    pub fn from_iter<T>(iter: T) -> Option<Box<Self>>
45    where
46        T: IntoIterator<Item = String>,
47    {
48        let set = iter.into_iter().collect();
49        Some(Box::new(ValueSetOauthScope { set }))
50    }
51}
52
53impl ValueSetScimPut for ValueSetOauthScope {
54    fn from_scim_json_put(value: JsonValue) -> Result<ValueSetResolveStatus, OperationError> {
55        let set: BTreeSet<String> = serde_json::from_value(value).map_err(|err| {
56            error!(?err, "SCIM Oauth2Scope syntax invalid");
57            OperationError::SC0019Oauth2ScopeSyntaxInvalid
58        })?;
59
60        Ok(ValueSetResolveStatus::Resolved(Box::new(
61            ValueSetOauthScope { set },
62        )))
63    }
64}
65
66impl ValueSetT for ValueSetOauthScope {
67    fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
68        match value {
69            Value::OauthScope(s) => Ok(self.set.insert(s)),
70            _ => {
71                debug_assert!(false);
72                Err(OperationError::InvalidValueState)
73            }
74        }
75    }
76
77    fn clear(&mut self) {
78        self.set.clear();
79    }
80
81    fn remove(&mut self, pv: &PartialValue, _cid: &Cid) -> bool {
82        match pv {
83            PartialValue::OauthScope(s) => self.set.remove(s.as_str()),
84            _ => {
85                debug_assert!(false);
86                true
87            }
88        }
89    }
90
91    fn contains(&self, pv: &PartialValue) -> bool {
92        match pv {
93            PartialValue::OauthScope(s) => self.set.contains(s.as_str()),
94            _ => false,
95        }
96    }
97
98    fn substring(&self, _pv: &PartialValue) -> bool {
99        false
100    }
101
102    fn startswith(&self, _pv: &PartialValue) -> bool {
103        false
104    }
105
106    fn endswith(&self, _pv: &PartialValue) -> bool {
107        false
108    }
109
110    fn lessthan(&self, _pv: &PartialValue) -> bool {
111        false
112    }
113
114    fn len(&self) -> usize {
115        self.set.len()
116    }
117
118    fn generate_idx_eq_keys(&self) -> Vec<String> {
119        self.set.iter().cloned().collect()
120    }
121
122    fn syntax(&self) -> SyntaxType {
123        SyntaxType::OauthScope
124    }
125
126    fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
127        self.set.iter().all(|s| OAUTHSCOPE_RE.is_match(s))
128    }
129
130    fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
131        Box::new(self.set.iter().cloned())
132    }
133
134    fn to_scim_value(&self) -> Option<ScimResolveStatus> {
135        Some(ScimResolveStatus::Resolved(ScimValueKanidm::ArrayString(
136            self.set.iter().cloned().collect(),
137        )))
138    }
139
140    fn to_db_valueset_v2(&self) -> DbValueSetV2 {
141        DbValueSetV2::OauthScope(self.set.iter().cloned().collect())
142    }
143
144    fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
145        Box::new(self.set.iter().cloned().map(PartialValue::OauthScope))
146    }
147
148    fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
149        Box::new(self.set.iter().cloned().map(Value::OauthScope))
150    }
151
152    fn equal(&self, other: &ValueSet) -> bool {
153        if let Some(other) = other.as_oauthscope_set() {
154            &self.set == other
155        } else {
156            debug_assert!(false);
157            false
158        }
159    }
160
161    fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> {
162        if let Some(b) = other.as_oauthscope_set() {
163            mergesets!(self.set, b)
164        } else {
165            debug_assert!(false);
166            Err(OperationError::InvalidValueState)
167        }
168    }
169
170    /*
171    fn to_oauthscope_single(&self) -> Option<&str> {
172        if self.set.len() == 1 {
173            self.set.iter().take(1).next().map(|s| s.as_str())
174        } else {
175            None
176        }
177    }
178    */
179
180    fn as_oauthscope_set(&self) -> Option<&BTreeSet<String>> {
181        Some(&self.set)
182    }
183
184    fn as_oauthscope_iter(&self) -> Option<Box<dyn Iterator<Item = &str> + '_>> {
185        Some(Box::new(self.set.iter().map(|s| s.as_str())))
186    }
187}
188
189#[derive(Debug, Clone)]
190pub struct ValueSetOauthScopeMap {
191    map: BTreeMap<Uuid, BTreeSet<String>>,
192}
193
194impl ValueSetOauthScopeMap {
195    pub fn new(u: Uuid, m: BTreeSet<String>) -> Box<Self> {
196        let mut map = BTreeMap::new();
197        map.insert(u, m);
198        Box::new(ValueSetOauthScopeMap { map })
199    }
200
201    pub fn push(&mut self, u: Uuid, m: BTreeSet<String>) -> bool {
202        self.map.insert(u, m).is_none()
203    }
204
205    pub fn from_dbvs2(data: Vec<DbValueOauthScopeMapV1>) -> Result<ValueSet, OperationError> {
206        let map = data
207            .into_iter()
208            .map(|DbValueOauthScopeMapV1 { refer, data }| (refer, data.into_iter().collect()))
209            .collect();
210        Ok(Box::new(ValueSetOauthScopeMap { map }))
211    }
212
213    // We need to allow this, because rust doesn't allow us to impl FromIterator on foreign
214    // types, and tuples are always foreign.
215    #[allow(clippy::should_implement_trait)]
216    pub fn from_iter<T>(iter: T) -> Option<Box<Self>>
217    where
218        T: IntoIterator<Item = (Uuid, BTreeSet<String>)>,
219    {
220        let map = iter.into_iter().collect();
221        Some(Box::new(ValueSetOauthScopeMap { map }))
222    }
223
224    pub(crate) fn from_set(resolved: Vec<ResolvedValueSetOauth2ScopeMap>) -> ValueSet {
225        let map = resolved
226            .into_iter()
227            .map(|ResolvedValueSetOauth2ScopeMap { group_uuid, scopes }| (group_uuid, scopes))
228            .collect();
229
230        Box::new(ValueSetOauthScopeMap { map })
231    }
232}
233
234impl ValueSetScimPut for ValueSetOauthScopeMap {
235    fn from_scim_json_put(value: JsonValue) -> Result<ValueSetResolveStatus, OperationError> {
236        let scope_maps: Vec<ClientScimOAuth2ScopeMap> =
237            serde_json::from_value(value).map_err(|err| {
238                error!(?err, "SCIM Oauth2ScopeMap syntax invalid");
239                OperationError::SC0020Oauth2ScopeMapSyntaxInvalid
240            })?;
241
242        // We make these both the same len as claim maps as during the resolve
243        // process we move everything from unresolved to resolved, and worst
244        // case is everything is unresolved.
245        let mut resolved = Vec::with_capacity(scope_maps.len());
246        let mut unresolved = Vec::with_capacity(scope_maps.len());
247
248        for ClientScimOAuth2ScopeMap {
249            group,
250            group_uuid,
251            scopes,
252        } in scope_maps.into_iter()
253        {
254            match (group_uuid, group) {
255                (None, None) => {
256                    error!("SCIM Oauth2ScopeMap a group name or uuid must be present");
257                    return Err(OperationError::SC0021Oauth2ScopeMapMissingGroupIdentifier);
258                }
259                (Some(group_uuid), _) => {
260                    resolved.push(ResolvedValueSetOauth2ScopeMap { group_uuid, scopes })
261                }
262                (None, Some(group_name)) => {
263                    unresolved.push(UnresolvedValueSetOauth2ScopeMap { group_name, scopes })
264                }
265            }
266        }
267
268        Ok(ValueSetResolveStatus::NeedsResolution(
269            ValueSetIntermediate::Oauth2ScopeMap {
270                resolved,
271                unresolved,
272            },
273        ))
274    }
275}
276
277impl ValueSetT for ValueSetOauthScopeMap {
278    fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
279        match value {
280            Value::OauthScopeMap(u, m) => {
281                match self.map.entry(u) {
282                    // We are going to assume that a vacant entry will not be set to empty.
283                    BTreeEntry::Vacant(e) => {
284                        e.insert(m);
285                        Ok(true)
286                    }
287                    // In the case that the value already exists, we update it. This is a quirk
288                    // of the oauth2 scope map type where add_ava assumes that a value's entire state
289                    // will be reflected, but we were only checking the *uuid* existed, not it's
290                    // associated map state. So by always replacing on a present, we are true to
291                    // the intent of the api.
292                    BTreeEntry::Occupied(mut e) => {
293                        if m.is_empty() {
294                            e.remove();
295                        } else {
296                            e.insert(m);
297                        }
298
299                        Ok(true)
300                    }
301                }
302            }
303            _ => Err(OperationError::InvalidValueState),
304        }
305    }
306
307    fn clear(&mut self) {
308        self.map.clear();
309    }
310
311    fn remove(&mut self, pv: &PartialValue, _cid: &Cid) -> bool {
312        match pv {
313            PartialValue::Refer(u) => self.map.remove(u).is_some(),
314            _ => false,
315        }
316    }
317
318    fn contains(&self, pv: &PartialValue) -> bool {
319        match pv {
320            PartialValue::Refer(u) => self.map.contains_key(u),
321            _ => false,
322        }
323    }
324
325    fn substring(&self, _pv: &PartialValue) -> bool {
326        false
327    }
328
329    fn startswith(&self, _pv: &PartialValue) -> bool {
330        false
331    }
332
333    fn endswith(&self, _pv: &PartialValue) -> bool {
334        false
335    }
336
337    fn lessthan(&self, _pv: &PartialValue) -> bool {
338        false
339    }
340
341    fn len(&self) -> usize {
342        self.map.len()
343    }
344
345    fn generate_idx_eq_keys(&self) -> Vec<String> {
346        self.map
347            .keys()
348            .map(|u| u.as_hyphenated().to_string())
349            .collect()
350    }
351
352    fn syntax(&self) -> SyntaxType {
353        SyntaxType::OauthScopeMap
354    }
355
356    fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
357        self.map
358            .values()
359            .flat_map(|set| set.iter())
360            .all(|s| OAUTHSCOPE_RE.is_match(s))
361    }
362
363    fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
364        Box::new(
365            self.map
366                .iter()
367                .map(|(u, m)| format!("{}: {:?}", uuid_to_proto_string(*u), m)),
368        )
369    }
370
371    fn to_scim_value(&self) -> Option<ScimResolveStatus> {
372        let unresolved_maps = self
373            .map
374            .iter()
375            .map(|(group_uuid, scopes)| UnresolvedScimValueOauth2ScopeMap {
376                group_uuid: *group_uuid,
377                scopes: scopes.clone(),
378            })
379            .collect::<Vec<_>>();
380
381        Some(ScimResolveStatus::NeedsResolution(
382            ScimValueIntermediate::Oauth2ScopeMap(unresolved_maps),
383        ))
384    }
385
386    fn to_db_valueset_v2(&self) -> DbValueSetV2 {
387        DbValueSetV2::OauthScopeMap(
388            self.map
389                .iter()
390                .map(|(u, m)| DbValueOauthScopeMapV1 {
391                    refer: *u,
392                    data: m.iter().cloned().collect(),
393                })
394                .collect(),
395        )
396    }
397
398    fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
399        Box::new(self.map.keys().cloned().map(PartialValue::Refer))
400    }
401
402    fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
403        Box::new(
404            self.map
405                .iter()
406                .map(|(u, m)| Value::OauthScopeMap(*u, m.clone())),
407        )
408    }
409
410    fn equal(&self, other: &ValueSet) -> bool {
411        if let Some(other) = other.as_oauthscopemap() {
412            &self.map == other
413        } else {
414            debug_assert!(false);
415            false
416        }
417    }
418
419    fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> {
420        if let Some(b) = other.as_oauthscopemap() {
421            mergemaps!(self.map, b)
422        } else {
423            debug_assert!(false);
424            Err(OperationError::InvalidValueState)
425        }
426    }
427
428    fn as_oauthscopemap(&self) -> Option<&BTreeMap<Uuid, BTreeSet<String>>> {
429        Some(&self.map)
430    }
431
432    fn as_ref_uuid_iter(&self) -> Option<Box<dyn Iterator<Item = Uuid> + '_>> {
433        // This is what ties us as a type that can be refint checked.
434        Some(Box::new(self.map.keys().copied()))
435    }
436}
437
438#[derive(Debug, Clone, PartialEq)]
439pub struct OauthClaimMapping {
440    join: OauthClaimMapJoin,
441    values: BTreeMap<Uuid, BTreeSet<String>>,
442}
443
444impl OauthClaimMapping {
445    pub(crate) fn join(&self) -> OauthClaimMapJoin {
446        self.join
447    }
448
449    pub(crate) fn values(&self) -> &BTreeMap<Uuid, BTreeSet<String>> {
450        &self.values
451    }
452}
453
454#[derive(Debug, Clone)]
455pub struct ValueSetOauthClaimMap {
456    //            Claim Name
457    map: BTreeMap<String, OauthClaimMapping>,
458}
459
460impl ValueSetOauthClaimMap {
461    pub(crate) fn new(claim: String, join: OauthClaimMapJoin) -> Box<Self> {
462        let mapping = OauthClaimMapping {
463            join,
464            values: BTreeMap::default(),
465        };
466        let mut map = BTreeMap::new();
467        map.insert(claim, mapping);
468        Box::new(ValueSetOauthClaimMap { map })
469    }
470
471    pub(crate) fn new_value(claim: String, group: Uuid, claims: BTreeSet<String>) -> Box<Self> {
472        let mut values = BTreeMap::default();
473        values.insert(group, claims);
474
475        let mapping = OauthClaimMapping {
476            join: OauthClaimMapJoin::default(),
477            values,
478        };
479
480        let mut map = BTreeMap::new();
481        map.insert(claim, mapping);
482        Box::new(ValueSetOauthClaimMap { map })
483    }
484
485    pub(crate) fn from_dbvs2(data: Vec<DbValueOauthClaimMap>) -> Result<ValueSet, OperationError> {
486        let map = data
487            .into_iter()
488            .map(|db_claim_map| match db_claim_map {
489                DbValueOauthClaimMap::V1 { name, join, values } => (
490                    name.clone(),
491                    OauthClaimMapping {
492                        join: join.into(),
493                        values: values.clone(),
494                    },
495                ),
496            })
497            .collect();
498        Ok(Box::new(ValueSetOauthClaimMap { map }))
499    }
500
501    pub(crate) fn from_set(resolved: Vec<ResolvedValueSetOauth2ClaimMap>) -> ValueSet {
502        let mut map = BTreeMap::new();
503
504        for ResolvedValueSetOauth2ClaimMap {
505            group_uuid,
506            claim,
507            join_char,
508            claim_values,
509        } in resolved.into_iter()
510        {
511            match map.entry(claim) {
512                BTreeEntry::Vacant(e) => {
513                    let mut values = BTreeMap::default();
514                    values.insert(group_uuid, claim_values);
515
516                    let claim_map = OauthClaimMapping {
517                        join: join_char,
518                        values,
519                    };
520                    e.insert(claim_map);
521                }
522                BTreeEntry::Occupied(mut e) => {
523                    // Just add the uuid/value, this claim name already exists.
524                    let mapping_mut = e.get_mut();
525                    match mapping_mut.values.entry(group_uuid) {
526                        BTreeEntry::Vacant(e) => {
527                            e.insert(claim_values);
528                        }
529                        BTreeEntry::Occupied(mut e) => {
530                            e.insert(claim_values);
531                        }
532                    }
533                }
534            }
535        }
536
537        Box::new(ValueSetOauthClaimMap { map })
538    }
539
540    fn trim(&mut self) {
541        self.map
542            .values_mut()
543            .for_each(|mapping_mut| mapping_mut.values.retain(|_k, v| !v.is_empty()));
544
545        self.map.retain(|_k, v| !v.values.is_empty());
546    }
547}
548
549impl ValueSetScimPut for ValueSetOauthClaimMap {
550    fn from_scim_json_put(value: JsonValue) -> Result<ValueSetResolveStatus, OperationError> {
551        let claim_maps: Vec<ClientScimOAuth2ClaimMap> =
552            serde_json::from_value(value).map_err(|err| {
553                error!(?err, "SCIM Oauth2ClaimMap syntax invalid");
554                OperationError::SC0022Oauth2ClaimMapSyntaxInvalid
555            })?;
556
557        // We make these both the same len as claim maps as during the resolve
558        // process we move everything from unresolved to resolved, and worst
559        // case is everything is unresolved.
560        let mut resolved = Vec::with_capacity(claim_maps.len());
561        let mut unresolved = Vec::with_capacity(claim_maps.len());
562
563        for ClientScimOAuth2ClaimMap {
564            group,
565            group_uuid,
566            claim,
567            join_char,
568            values: claim_values,
569        } in claim_maps.into_iter()
570        {
571            let join_char = OauthClaimMapJoin::from(join_char);
572
573            match (group_uuid, group) {
574                (None, None) => {
575                    error!("SCIM Oauth2ClaimMap a group name or uuid must be present");
576                    return Err(OperationError::SC0023Oauth2ClaimMapMissingGroupIdentifier);
577                }
578                (Some(group_uuid), _) => resolved.push(ResolvedValueSetOauth2ClaimMap {
579                    group_uuid,
580                    claim,
581                    join_char,
582                    claim_values,
583                }),
584                (None, Some(group_name)) => unresolved.push(UnresolvedValueSetOauth2ClaimMap {
585                    group_name,
586                    claim,
587                    join_char,
588                    claim_values,
589                }),
590            }
591        }
592
593        Ok(ValueSetResolveStatus::NeedsResolution(
594            ValueSetIntermediate::Oauth2ClaimMap {
595                resolved,
596                unresolved,
597            },
598        ))
599    }
600}
601
602impl ValueSetT for ValueSetOauthClaimMap {
603    fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
604        match value {
605            Value::OauthClaimValue(name, uuid, claims) => {
606                // Add a value to this group associated to this claim.
607                match self.map.entry(name) {
608                    BTreeEntry::Vacant(e) => {
609                        // New map/value. Use a default joiner.
610                        let mut values = BTreeMap::default();
611                        values.insert(uuid, claims);
612
613                        let claim_map = OauthClaimMapping {
614                            join: OauthClaimMapJoin::default(),
615                            values,
616                        };
617                        e.insert(claim_map);
618                        Ok(true)
619                    }
620                    BTreeEntry::Occupied(mut e) => {
621                        // Just add the uuid/value, this claim name already exists.
622                        let mapping_mut = e.get_mut();
623                        match mapping_mut.values.entry(uuid) {
624                            BTreeEntry::Vacant(e) => {
625                                e.insert(claims);
626                                Ok(true)
627                            }
628                            BTreeEntry::Occupied(mut e) => {
629                                e.insert(claims);
630                                Ok(true)
631                            }
632                        }
633                    }
634                }
635            }
636            Value::OauthClaimMap(name, join) => {
637                match self.map.entry(name) {
638                    BTreeEntry::Vacant(e) => {
639                        // Create a new empty claim mapping.
640                        let claim_map = OauthClaimMapping {
641                            join,
642                            values: BTreeMap::default(),
643                        };
644                        e.insert(claim_map);
645                        Ok(true)
646                    }
647                    BTreeEntry::Occupied(mut e) => {
648                        // Just update the join strategy.
649                        e.get_mut().join = join;
650                        Ok(true)
651                    }
652                }
653            }
654            _ => Err(OperationError::InvalidValueState),
655        }
656    }
657
658    fn clear(&mut self) {
659        self.map.clear();
660    }
661
662    fn remove(&mut self, pv: &PartialValue, _cid: &Cid) -> bool {
663        let res = match pv {
664            // Remove this claim as a whole
665            PartialValue::Iutf8(s) => self.map.remove(s).is_some(),
666            // Remove all references to this group from this claim map.
667            PartialValue::Refer(u) => {
668                let mut contained = false;
669                for mapping_mut in self.map.values_mut() {
670                    contained |= mapping_mut.values.remove(u).is_some();
671                }
672                contained
673            }
674            PartialValue::OauthClaim(s, u) => {
675                // Remove a uuid from this claim type.
676                if let Some(mapping_mut) = self.map.get_mut(s) {
677                    mapping_mut.values.remove(u).is_some()
678                } else {
679                    false
680                }
681            }
682            PartialValue::OauthClaimValue(s, u, v) => {
683                // Remove a value from this uuid, associated to this claim name.
684                if let Some(mapping_mut) = self.map.get_mut(s) {
685                    if let Some(claim_mut) = mapping_mut.values.get_mut(u) {
686                        claim_mut.remove(v)
687                    } else {
688                        false
689                    }
690                } else {
691                    false
692                }
693            }
694            _ => false,
695        };
696
697        // Trim anything that is now empty.
698        self.trim();
699
700        res
701    }
702
703    fn contains(&self, pv: &PartialValue) -> bool {
704        match pv {
705            PartialValue::Iutf8(s) => self.map.contains_key(s),
706            PartialValue::Refer(u) => {
707                let mut contained = false;
708                for mapping in self.map.values() {
709                    contained |= mapping.values.contains_key(u);
710                }
711                contained
712            }
713            _ => false,
714        }
715    }
716
717    fn substring(&self, _pv: &PartialValue) -> bool {
718        false
719    }
720
721    fn startswith(&self, _pv: &PartialValue) -> bool {
722        false
723    }
724
725    fn endswith(&self, _pv: &PartialValue) -> bool {
726        false
727    }
728
729    fn lessthan(&self, _pv: &PartialValue) -> bool {
730        false
731    }
732
733    fn len(&self) -> usize {
734        self.map.len()
735    }
736
737    fn generate_idx_eq_keys(&self) -> Vec<String> {
738        self.map
739            .keys()
740            .cloned()
741            .chain(
742                self.map.values().flat_map(|mapping| {
743                    mapping.values.keys().map(|u| u.as_hyphenated().to_string())
744                }),
745            )
746            .collect()
747    }
748
749    fn syntax(&self) -> SyntaxType {
750        SyntaxType::OauthClaimMap
751    }
752
753    fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
754        self.map.keys().all(|s| OAUTHSCOPE_RE.is_match(s))
755            && self
756                .map
757                .values()
758                .flat_map(|mapping| {
759                    mapping
760                        .values
761                        .values()
762                        .map(|claim_values| claim_values.is_empty())
763                })
764                .all(|is_empty| !is_empty)
765            && self
766                .map
767                .values()
768                .flat_map(|mapping| {
769                    mapping
770                        .values
771                        .values()
772                        .flat_map(|claim_values| claim_values.iter())
773                })
774                .all(|s| OAUTHSCOPE_RE.is_match(s))
775    }
776
777    fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
778        Box::new(self.map.iter().flat_map(|(name, mapping)| {
779            mapping.values.iter().map(move |(group, claims)| {
780                let join_str = mapping.join.to_str();
781
782                let joined = str_concat!(claims, join_str);
783
784                format!(
785                    "{}: {} \"{:?}\"",
786                    name,
787                    uuid_to_proto_string(*group),
788                    joined
789                )
790            })
791        }))
792    }
793
794    fn to_scim_value(&self) -> Option<ScimResolveStatus> {
795        let unresolved_maps = self
796            .map
797            .iter()
798            .flat_map(|(claim_name, mappings)| {
799                mappings.values.iter().map(|(group_uuid, claim_values)| {
800                    UnresolvedScimValueOauth2ClaimMap {
801                        group_uuid: *group_uuid,
802                        claim: claim_name.to_string(),
803                        join_char: mappings.join.into(),
804                        values: claim_values.clone(),
805                    }
806                })
807            })
808            .collect::<Vec<_>>();
809
810        Some(ScimResolveStatus::NeedsResolution(
811            ScimValueIntermediate::Oauth2ClaimMap(unresolved_maps),
812        ))
813    }
814
815    fn to_db_valueset_v2(&self) -> DbValueSetV2 {
816        DbValueSetV2::OauthClaimMap(
817            self.map
818                .iter()
819                .map(|(name, mapping)| DbValueOauthClaimMap::V1 {
820                    name: name.clone(),
821                    join: mapping.join.into(),
822                    values: mapping.values.clone(),
823                })
824                .collect(),
825        )
826    }
827
828    fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
829        Box::new(self.map.keys().cloned().map(PartialValue::Iutf8))
830    }
831
832    fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
833        debug_assert!(false);
834        Box::new(
835            std::iter::empty(), /*
836                                self.map
837                                    .iter()
838                                    .map(|(u, m)| Value::OauthScopeMap(*u, m.clone())),
839                                */
840        )
841    }
842
843    fn equal(&self, other: &ValueSet) -> bool {
844        if let Some(other) = other.as_oauthclaim_map() {
845            &self.map == other
846        } else {
847            debug_assert!(false);
848            false
849        }
850    }
851
852    fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> {
853        if let Some(b) = other.as_oauthclaim_map() {
854            mergemaps!(self.map, b)
855        } else {
856            debug_assert!(false);
857            Err(OperationError::InvalidValueState)
858        }
859    }
860
861    fn as_oauthclaim_map(&self) -> Option<&BTreeMap<String, OauthClaimMapping>> {
862        Some(&self.map)
863    }
864
865    fn as_ref_uuid_iter(&self) -> Option<Box<dyn Iterator<Item = Uuid> + '_>> {
866        // This is what ties us as a type that can be refint checked.
867        Some(Box::new(
868            self.map
869                .values()
870                .flat_map(|mapping| mapping.values.keys())
871                .copied(),
872        ))
873    }
874}
875
876#[cfg(test)]
877mod tests {
878    use super::{ValueSetOauthClaimMap, ValueSetOauthScope, ValueSetOauthScopeMap};
879    use crate::prelude::*;
880    use std::collections::BTreeSet;
881
882    #[test]
883    fn test_oauth_claim_invalid_str_concat_when_empty() {
884        let group_uuid = uuid::uuid!("5a6b8783-3f67-4ebb-b6aa-77fd6e66589f");
885        let vs =
886            ValueSetOauthClaimMap::new_value("claim".to_string(), group_uuid, BTreeSet::default());
887
888        // Invalid handling of an empty claim map would cause a crash.
889        let proto_value = vs.to_proto_string_clone_iter().next().unwrap();
890
891        assert_eq!(
892            &proto_value,
893            "claim: 5a6b8783-3f67-4ebb-b6aa-77fd6e66589f \"\"\"\""
894        );
895    }
896
897    #[test]
898    fn test_scim_oauth2_scope() {
899        let vs: ValueSet = ValueSetOauthScope::new("fully_sick_scope_m8".to_string());
900        let data = r#"["fully_sick_scope_m8"]"#;
901        crate::valueset::scim_json_reflexive(&vs, data);
902
903        // Test that we can parse json values into a valueset.
904        crate::valueset::scim_json_put_reflexive::<ValueSetOauthScope>(&vs, &[])
905    }
906
907    #[qs_test]
908    async fn test_scim_oauth2_scope_map(server: &QueryServer) {
909        let mut write_txn = server.write(duration_from_epoch_now()).await.unwrap();
910
911        let g_uuid = uuid::uuid!("4d21d04a-dc0e-42eb-b850-34dd180b107f");
912        assert!(write_txn
913            .internal_create(vec![entry_init!(
914                (Attribute::Class, EntryClass::Object.to_value()),
915                (Attribute::Class, EntryClass::Group.to_value()),
916                (Attribute::Name, Value::new_iname("testgroup")),
917                (Attribute::Uuid, Value::Uuid(g_uuid))
918            ),])
919            .is_ok());
920
921        let set = ["read".to_string(), "write".to_string()].into();
922        let vs: ValueSet = ValueSetOauthScopeMap::new(g_uuid, set);
923
924        let data = r#"
925[
926  {
927    "scopes": ["read", "write"],
928    "group": "testgroup@example.com",
929    "groupUuid": "4d21d04a-dc0e-42eb-b850-34dd180b107f"
930  }
931]
932        "#;
933        crate::valueset::scim_json_reflexive_unresolved(&mut write_txn, &vs, data);
934
935        // Test that we can parse json values into a valueset.
936        crate::valueset::scim_json_put_reflexive_unresolved::<ValueSetOauthScopeMap>(
937            &mut write_txn,
938            &vs,
939            &[],
940        );
941
942        assert!(write_txn.commit().is_ok());
943    }
944
945    #[qs_test]
946    async fn test_scim_oauth2_claim_map(server: &QueryServer) {
947        let mut write_txn = server.write(duration_from_epoch_now()).await.unwrap();
948
949        let g_uuid = uuid::uuid!("4d21d04a-dc0e-42eb-b850-34dd180b107f");
950        assert!(write_txn
951            .internal_create(vec![entry_init!(
952                (Attribute::Class, EntryClass::Object.to_value()),
953                (Attribute::Class, EntryClass::Group.to_value()),
954                (Attribute::Name, Value::new_iname("testgroup")),
955                (Attribute::Uuid, Value::Uuid(g_uuid))
956            ),])
957            .is_ok());
958
959        let set = ["read".to_string(), "write".to_string()].into();
960        let vs: ValueSet = ValueSetOauthClaimMap::new_value("claim".to_string(), g_uuid, set);
961
962        let data = r#"
963[
964  {
965    "claim": "claim",
966    "group": "testgroup@example.com",
967    "groupUuid": "4d21d04a-dc0e-42eb-b850-34dd180b107f",
968    "joinChar": ";",
969    "values": ["read", "write"]
970  }
971]
972        "#;
973        crate::valueset::scim_json_reflexive_unresolved(&mut write_txn, &vs, data);
974
975        // Test that we can parse json values into a valueset.
976        crate::valueset::scim_json_put_reflexive_unresolved::<ValueSetOauthClaimMap>(
977            &mut write_txn,
978            &vs,
979            &[],
980        );
981
982        assert!(write_txn.commit().is_ok());
983    }
984}