Skip to main content

kanidm_proto/scim_v1/
mod.rs

1//! These represent Kanidm's view of SCIM resources that a client will serialise
2//! for transmission, and the server will deserialise to process them. In reverse
3//! Kanidm will send responses that a client can then process and use.
4//!
5//! A challenge of this is that it creates an asymmetry between the client and server
6//! as SCIM contains very few strong types. Without awareness of what the client
7//! or server intended it's not possible to directly deserialise into a rust
8//! strong type on the receiver. To resolve this, this library divides the elements
9//! into multiple parts.
10//!
11//! The [scim_proto] library, which is generic over all scim implementations.
12//!
13//! The client module, which describes how a client should transmit entries, and
14//! how it should parse them when it receives them.
15//!
16//! The server module, which describes how a server should transmit entries and
17//! how it should receive them.
18
19use crate::attribute::{Attribute, SubAttribute};
20use serde::{Deserialize, Serialize};
21use serde_with::formats::CommaSeparator;
22use serde_with::{serde_as, skip_serializing_none, DisplayFromStr, StringWithSeparator};
23use sshkey_attest::proto::PublicKey as SshPublicKey;
24use std::collections::BTreeMap;
25use std::fmt;
26use std::num::NonZeroU64;
27use std::ops::Not;
28use std::str::FromStr;
29use utoipa::ToSchema;
30use uuid::Uuid;
31
32pub use self::synch::*;
33pub use scim_proto::prelude::*;
34pub use serde_json::Value as JsonValue;
35
36pub mod client;
37pub mod server;
38mod synch;
39
40/// A generic ScimEntry. This retains attribute
41/// values in a generic state awaiting processing by schema aware transforms
42/// either by the server or the client.
43#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
44pub struct ScimEntryGeneric {
45    #[serde(flatten)]
46    pub header: ScimEntryHeader,
47    #[serde(flatten)]
48    pub attrs: BTreeMap<Attribute, JsonValue>,
49}
50
51#[derive(Serialize, Deserialize, Clone, Debug, Default, ToSchema)]
52#[serde(rename_all = "lowercase")]
53pub enum ScimSortOrder {
54    #[default]
55    Ascending,
56    Descending,
57}
58
59/// SCIM Query Parameters used during the get of a single entry
60#[serde_as]
61#[skip_serializing_none]
62#[derive(Serialize, Deserialize, Clone, Debug, Default, ToSchema)]
63#[serde(rename_all = "camelCase")]
64pub struct ScimEntryGetQuery {
65    #[serde_as(as = "Option<StringWithSeparator::<CommaSeparator, Attribute>>")]
66    pub attributes: Option<Vec<Attribute>>,
67    #[serde(default, skip_serializing_if = "<&bool>::not")]
68    pub ext_access_check: bool,
69
70    // Sorting per https://www.rfc-editor.org/rfc/rfc7644#section-3.4.2.3
71    #[serde(default)]
72    pub sort_by: Option<Attribute>,
73    #[serde(default)]
74    pub sort_order: Option<ScimSortOrder>,
75
76    // Pagination https://www.rfc-editor.org/rfc/rfc7644#section-3.4.2.4
77    #[schema(value_type = u64)]
78    pub start_index: Option<NonZeroU64>,
79    #[schema(value_type = u64)]
80    pub count: Option<NonZeroU64>,
81
82    // Strongly typed filter (rather than generic)
83    #[serde_as(as = "Option<DisplayFromStr>")]
84    #[schema(value_type = JsonValue)]
85    pub filter: Option<ScimFilter>,
86}
87
88#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
89pub enum ScimSchema {
90    #[serde(rename = "urn:ietf:params:scim:schemas:kanidm:sync:1:account")]
91    SyncAccountV1,
92    #[serde(rename = "urn:ietf:params:scim:schemas:kanidm:sync:1:group")]
93    SyncV1GroupV1,
94    #[serde(rename = "urn:ietf:params:scim:schemas:kanidm:sync:1:person")]
95    SyncV1PersonV1,
96    #[serde(rename = "urn:ietf:params:scim:schemas:kanidm:sync:1:posixaccount")]
97    SyncV1PosixAccountV1,
98    #[serde(rename = "urn:ietf:params:scim:schemas:kanidm:sync:1:posixgroup")]
99    SyncV1PosixGroupV1,
100}
101
102#[serde_as]
103#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone, ToSchema)]
104#[serde(deny_unknown_fields, rename_all = "camelCase")]
105pub struct ScimMail {
106    #[serde(default)]
107    pub primary: bool,
108    pub value: String,
109}
110
111#[derive(Deserialize, Serialize, Debug, Clone, ToSchema)]
112#[serde(rename_all = "camelCase")]
113pub struct ScimSshPublicKey {
114    pub label: String,
115
116    #[schema(value_type = String)]
117    pub value: SshPublicKey,
118}
119
120#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, ToSchema)]
121#[serde(rename_all = "camelCase")]
122pub struct ScimReference {
123    pub uuid: Uuid,
124    pub value: String,
125}
126
127#[derive(Deserialize, Serialize, Debug, Clone, ToSchema)]
128pub enum ScimOauth2ClaimMapJoinChar {
129    #[serde(rename = ",", alias = "csv")]
130    CommaSeparatedValue,
131    #[serde(rename = " ", alias = "ssv")]
132    SpaceSeparatedValue,
133    #[serde(rename = ";", alias = "json_array")]
134    JsonArray,
135}
136
137#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, ToSchema)]
138#[serde(rename_all = "camelCase")]
139pub struct ScimApplicationPassword {
140    pub uuid: Uuid,
141    pub label: String,
142    pub secret: String,
143}
144
145#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, ToSchema)]
146#[serde(rename_all = "camelCase")]
147pub struct ScimApplicationPasswordCreate {
148    pub application_uuid: Uuid,
149    pub label: String,
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, Deserialize, ToSchema)]
153pub struct AttrPath {
154    pub a: Attribute,
155    pub s: Option<SubAttribute>,
156}
157
158impl fmt::Display for AttrPath {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        if let Some(subattr) = self.s.as_ref() {
161            write!(f, "{}.{}", self.a, subattr)
162        } else {
163            write!(f, "{}", self.a)
164        }
165    }
166}
167
168impl From<Attribute> for AttrPath {
169    fn from(a: Attribute) -> Self {
170        Self { a, s: None }
171    }
172}
173
174impl From<(Attribute, SubAttribute)> for AttrPath {
175    fn from((a, s): (Attribute, SubAttribute)) -> Self {
176        Self { a, s: Some(s) }
177    }
178}
179
180#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
181pub enum ScimFilter {
182    Or(Box<ScimFilter>, Box<ScimFilter>),
183    And(Box<ScimFilter>, Box<ScimFilter>),
184    Not(Box<ScimFilter>),
185
186    Present(AttrPath),
187    Equal(AttrPath, JsonValue),
188    NotEqual(AttrPath, JsonValue),
189    Contains(AttrPath, JsonValue),
190    StartsWith(AttrPath, JsonValue),
191    EndsWith(AttrPath, JsonValue),
192    Greater(AttrPath, JsonValue),
193    Less(AttrPath, JsonValue),
194    GreaterOrEqual(AttrPath, JsonValue),
195    LessOrEqual(AttrPath, JsonValue),
196
197    Complex(Attribute, Box<ScimComplexFilter>),
198}
199
200impl fmt::Display for ScimFilter {
201    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202        match self {
203            Self::Equal(attrpath, value) => write!(f, "({attrpath} eq {value})"),
204            Self::Contains(attrpath, value) => write!(f, "({attrpath} co {value})"),
205            Self::Not(expr) => write!(f, "(not ({expr}))"),
206            Self::Or(this, that) => write!(f, "({this} or {that})"),
207            Self::And(this, that) => write!(f, "({this} and {that})"),
208            Self::EndsWith(attrpath, value) => write!(f, "({attrpath} ew {value})"),
209            Self::Greater(attrpath, value) => write!(f, "({attrpath} gt {value})"),
210            Self::GreaterOrEqual(attrpath, value) => {
211                write!(f, "({attrpath} ge {value})")
212            }
213            Self::Less(attrpath, value) => write!(f, "({attrpath} lt {value})"),
214            Self::LessOrEqual(attrpath, value) => write!(f, "({attrpath} le {value})"),
215            Self::NotEqual(attrpath, value) => write!(f, "({attrpath} ne {value})"),
216            Self::Present(attrpath) => write!(f, "({attrpath} pr)"),
217            Self::StartsWith(attrpath, value) => write!(f, "({attrpath} sw {value})"),
218            Self::Complex(attrname, expr) => write!(f, "{attrname}[{expr}]"),
219        }
220    }
221}
222
223#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
224pub enum ScimComplexFilter {
225    Or(Box<ScimComplexFilter>, Box<ScimComplexFilter>),
226    And(Box<ScimComplexFilter>, Box<ScimComplexFilter>),
227    Not(Box<ScimComplexFilter>),
228
229    Present(SubAttribute),
230    Equal(SubAttribute, JsonValue),
231    NotEqual(SubAttribute, JsonValue),
232    Contains(SubAttribute, JsonValue),
233    StartsWith(SubAttribute, JsonValue),
234    EndsWith(SubAttribute, JsonValue),
235    Greater(SubAttribute, JsonValue),
236    Less(SubAttribute, JsonValue),
237    GreaterOrEqual(SubAttribute, JsonValue),
238    LessOrEqual(SubAttribute, JsonValue),
239}
240
241impl fmt::Display for ScimComplexFilter {
242    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243        match self {
244            Self::Equal(subattr, value) => write!(f, "({subattr} eq {value})"),
245            Self::Contains(subattr, value) => write!(f, "({subattr} co {value})"),
246            Self::Not(expr) => write!(f, "(not ({expr}))"),
247            Self::Or(this, that) => write!(f, "({this} or {that})"),
248            Self::And(this, that) => write!(f, "({this} and {that})"),
249            Self::EndsWith(subattr, value) => write!(f, "({subattr} ew {value})"),
250            Self::Greater(subattr, value) => write!(f, "({subattr} gt {value})"),
251            Self::GreaterOrEqual(subattr, value) => {
252                write!(f, "({subattr} ge {value})")
253            }
254            Self::Less(subattr, value) => write!(f, "({subattr} lt {value})"),
255            Self::LessOrEqual(subattr, value) => write!(f, "({subattr} le {value})"),
256            Self::NotEqual(subattr, value) => write!(f, "({subattr} ne {value})"),
257            Self::Present(subattr) => write!(f, "({subattr} pr)"),
258            Self::StartsWith(subattr, value) => write!(f, "({subattr} sw {value})"),
259        }
260    }
261}
262
263const SCIM_FILTER_MAX_DEPTH: usize = 128;
264
265peg::parser! {
266    grammar scimfilter() for str {
267
268        pub rule parse() -> ScimFilter =
269            f:parse_depth(SCIM_FILTER_MAX_DEPTH) { f }
270
271        pub(crate) rule parse_depth(max_depth: usize) -> ScimFilter =
272            a:parse_inner(max_depth.saturating_sub(1)) {?
273                if max_depth == 0 { Err("too deeply nested") } else { Ok(a) }
274            }
275
276        rule parse_inner(max_depth: usize) -> ScimFilter = precedence!{
277            a:(@) separator()+ "or" separator()+ b:@ {
278                ScimFilter::Or(
279                    Box::new(a),
280                    Box::new(b)
281                )
282            }
283            --
284            a:(@) separator()+ "and" separator()+ b:@ {
285                ScimFilter::And(
286                    Box::new(a),
287                    Box::new(b)
288                )
289            }
290            --
291            "not" separator()+ "(" e:parse_depth(max_depth) ")" {
292                ScimFilter::Not(Box::new(e))
293            }
294            --
295            a:attrname()"[" e:parse_complex_depth(max_depth) "]" {
296                ScimFilter::Complex(
297                    a,
298                    Box::new(e)
299                )
300            }
301            --
302            a:attrexp() { a }
303            "(" e:parse_depth(max_depth) ")" { e }
304        }
305
306        pub rule parse_complex() -> ScimComplexFilter =
307            f:parse_complex_depth(SCIM_FILTER_MAX_DEPTH) { f }
308
309        pub(crate) rule parse_complex_depth(max_depth: usize) -> ScimComplexFilter =
310            a:parse_complex_inner(max_depth.saturating_sub(1)) {?
311                if max_depth == 0 { Err("too deeply nested") } else { Ok(a) }
312            }
313
314        rule parse_complex_inner(max_depth: usize) -> ScimComplexFilter = precedence!{
315            a:(@) separator()+ "or" separator()+ b:@ {
316                ScimComplexFilter::Or(
317                    Box::new(a),
318                    Box::new(b)
319                )
320            }
321            --
322            a:(@) separator()+ "and" separator()+ b:@ {
323                ScimComplexFilter::And(
324                    Box::new(a),
325                    Box::new(b)
326                )
327            }
328            --
329            "not" separator()+ "(" e:parse_complex_depth(max_depth) ")" {
330                ScimComplexFilter::Not(Box::new(e))
331            }
332            --
333            a:complex_attrexp() { a }
334            "(" e:parse_complex_depth(max_depth) ")" { e }
335        }
336
337        pub(crate) rule attrexp() -> ScimFilter =
338            pres()
339            / eq()
340            / ne()
341            / co()
342            / sw()
343            / ew()
344            / gt()
345            / lt()
346            / ge()
347            / le()
348
349        pub(crate) rule pres() -> ScimFilter =
350            a:attrpath() separator()+ "pr" { ScimFilter::Present(a) }
351
352        pub(crate) rule eq() -> ScimFilter =
353            a:attrpath() separator()+ "eq" separator()+ v:value() { ScimFilter::Equal(a, v) }
354
355        pub(crate) rule ne() -> ScimFilter =
356            a:attrpath() separator()+ "ne" separator()+ v:value() { ScimFilter::NotEqual(a, v) }
357
358        pub(crate) rule co() -> ScimFilter =
359            a:attrpath() separator()+ "co" separator()+ v:value() { ScimFilter::Contains(a, v) }
360
361        pub(crate) rule sw() -> ScimFilter =
362            a:attrpath() separator()+ "sw" separator()+ v:value() { ScimFilter::StartsWith(a, v) }
363
364        pub(crate) rule ew() -> ScimFilter =
365            a:attrpath() separator()+ "ew" separator()+ v:value() { ScimFilter::EndsWith(a, v) }
366
367        pub(crate) rule gt() -> ScimFilter =
368            a:attrpath() separator()+ "gt" separator()+ v:value() { ScimFilter::Greater(a, v) }
369
370        pub(crate) rule lt() -> ScimFilter =
371            a:attrpath() separator()+ "lt" separator()+ v:value() { ScimFilter::Less(a, v) }
372
373        pub(crate) rule ge() -> ScimFilter =
374            a:attrpath() separator()+ "ge" separator()+ v:value() { ScimFilter::GreaterOrEqual(a, v) }
375
376        pub(crate) rule le() -> ScimFilter =
377            a:attrpath() separator()+ "le" separator()+ v:value() { ScimFilter::LessOrEqual(a, v) }
378
379        pub(crate) rule complex_attrexp() -> ScimComplexFilter =
380            c_pres()
381            / c_eq()
382            / c_ne()
383            / c_co()
384            / c_sw()
385            / c_ew()
386            / c_gt()
387            / c_lt()
388            / c_ge()
389            / c_le()
390
391        pub(crate) rule c_pres() -> ScimComplexFilter =
392            a:subattr() separator()+ "pr" { ScimComplexFilter::Present(a) }
393
394        pub(crate) rule c_eq() -> ScimComplexFilter =
395            a:subattr() separator()+ "eq" separator()+ v:value() { ScimComplexFilter::Equal(a, v) }
396
397        pub(crate) rule c_ne() -> ScimComplexFilter =
398            a:subattr() separator()+ "ne" separator()+ v:value() { ScimComplexFilter::NotEqual(a, v) }
399
400        pub(crate) rule c_co() -> ScimComplexFilter =
401            a:subattr() separator()+ "co" separator()+ v:value() { ScimComplexFilter::Contains(a, v) }
402
403        pub(crate) rule c_sw() -> ScimComplexFilter =
404            a:subattr() separator()+ "sw" separator()+ v:value() { ScimComplexFilter::StartsWith(a, v) }
405
406        pub(crate) rule c_ew() -> ScimComplexFilter =
407            a:subattr() separator()+ "ew" separator()+ v:value() { ScimComplexFilter::EndsWith(a, v) }
408
409        pub(crate) rule c_gt() -> ScimComplexFilter =
410            a:subattr() separator()+ "gt" separator()+ v:value() { ScimComplexFilter::Greater(a, v) }
411
412        pub(crate) rule c_lt() -> ScimComplexFilter =
413            a:subattr() separator()+ "lt" separator()+ v:value() { ScimComplexFilter::Less(a, v) }
414
415        pub(crate) rule c_ge() -> ScimComplexFilter =
416            a:subattr() separator()+ "ge" separator()+ v:value() { ScimComplexFilter::GreaterOrEqual(a, v) }
417
418        pub(crate) rule c_le() -> ScimComplexFilter =
419            a:subattr() separator()+ "le" separator()+ v:value() { ScimComplexFilter::LessOrEqual(a, v) }
420
421        rule separator() =
422            ['\n' | ' ' | '\t' ]
423
424        rule operator() =
425            ['\n' | ' ' | '\t' | '(' | ')' | '[' | ']' ]
426
427        rule value() -> JsonValue =
428            quotedvalue() / unquotedvalue()
429
430        rule quotedvalue() -> JsonValue =
431            s:$(['"'] ((['\\'][_]) / (!['"'][_]))* ['"']) {? serde_json::from_str(s).map_err(|_| "invalid json value" ) }
432
433        rule unquotedvalue() -> JsonValue =
434            s:$((!operator()[_])*) {? serde_json::from_str(s).map_err(|_| "invalid json value" ) }
435
436        pub(crate) rule attrpath() -> AttrPath =
437            a:attrname() s:dot_subattr()? { AttrPath { a, s } }
438
439        rule dot_subattr() -> SubAttribute =
440            "." s:subattr() { s }
441
442        rule subattr() -> SubAttribute =
443            s:attrstring() { SubAttribute::from(s.as_str()) }
444
445        pub(crate) rule attrname() -> Attribute =
446            s:attrstring() { Attribute::from(s.as_str()) }
447
448        pub(crate) rule attrstring() -> String =
449            s:$([ 'a'..='z' | 'A'..='Z']['a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' ]*) { s.to_string() }
450    }
451}
452
453impl FromStr for AttrPath {
454    type Err = peg::error::ParseError<peg::str::LineCol>;
455    fn from_str(input: &str) -> Result<Self, Self::Err> {
456        scimfilter::attrpath(input)
457    }
458}
459
460impl FromStr for ScimFilter {
461    type Err = peg::error::ParseError<peg::str::LineCol>;
462    fn from_str(input: &str) -> Result<Self, Self::Err> {
463        scimfilter::parse(input)
464    }
465}
466
467impl FromStr for ScimComplexFilter {
468    type Err = peg::error::ParseError<peg::str::LineCol>;
469    fn from_str(input: &str) -> Result<Self, Self::Err> {
470        scimfilter::parse_complex(input)
471    }
472}
473
474#[cfg(test)]
475mod tests {
476    use super::*;
477
478    #[test]
479    fn scim_rfc_to_generic() {
480        // Assert that we can transition from the rfc generic entries to the
481        // kanidm types.
482    }
483
484    #[test]
485    fn scim_kani_to_generic() {
486        // Assert that a kanidm strong entry can convert to generic.
487    }
488
489    #[test]
490    fn scim_kani_to_rfc() {
491        // Assert that a kanidm strong entry can convert to rfc.
492    }
493
494    #[test]
495    fn scim_sync_kani_to_rfc() {
496        use super::*;
497
498        // Group
499        let group_uuid = uuid::uuid!("2d0a9e7c-cc08-4ca2-8d7f-114f9abcfc8a");
500
501        let group = ScimSyncGroup::builder(
502            group_uuid,
503            "cn=testgroup".to_string(),
504            "testgroup".to_string(),
505        )
506        .set_description(Some("test desc".to_string()))
507        .set_gidnumber(Some(12345))
508        .set_members(vec!["member_a".to_string(), "member_a".to_string()].into_iter())
509        .build();
510
511        let entry: Result<ScimEntry, _> = group.try_into();
512
513        assert!(entry.is_ok());
514
515        // User
516        let user_uuid = uuid::uuid!("cb3de098-33fd-4565-9d80-4f7ed6a664e9");
517
518        let user_sshkey = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBENubZikrb8hu+HeVRdZ0pp/VAk2qv4JDbuJhvD0yNdWDL2e3cBbERiDeNPkWx58Q4rVnxkbV1fa8E2waRtT91wAAAAEc3NoOg== testuser@fidokey";
519
520        let person = ScimSyncPerson::builder(
521            user_uuid,
522            "cn=testuser".to_string(),
523            "testuser".to_string(),
524            "Test User".to_string(),
525        )
526        .set_password_import(Some("new_password".to_string()))
527        .set_unix_password_import(Some("new_password".to_string()))
528        .set_totp_import(vec![ScimTotp {
529            external_id: "Totp".to_string(),
530            secret: "abcd".to_string(),
531            algo: "SHA3".to_string(),
532            step: 60,
533            digits: 8,
534        }])
535        .set_mail(vec![MultiValueAttr {
536            primary: Some(true),
537            value: "testuser@example.com".to_string(),
538            ..Default::default()
539        }])
540        .set_ssh_publickey(vec![ScimSshPubKey {
541            label: "Key McKeyface".to_string(),
542            value: user_sshkey.to_string(),
543        }])
544        .set_login_shell(Some("/bin/false".to_string()))
545        .set_account_valid_from(Some("2023-11-28T04:57:55Z".to_string()))
546        .set_account_expire(Some("2023-11-28T04:57:55Z".to_string()))
547        .set_gidnumber(Some(54321))
548        .build();
549
550        let entry: Result<ScimEntry, _> = person.try_into();
551
552        assert!(entry.is_ok());
553    }
554
555    #[test]
556    fn scim_entry_get_query() {
557        use super::*;
558
559        let q = ScimEntryGetQuery {
560            attributes: None,
561            ..Default::default()
562        };
563
564        let txt = serde_urlencoded::to_string(&q).unwrap();
565
566        assert_eq!(txt, "");
567
568        let q = ScimEntryGetQuery {
569            attributes: Some(vec![Attribute::Name]),
570            ext_access_check: false,
571            ..Default::default()
572        };
573
574        let txt = serde_urlencoded::to_string(&q).unwrap();
575        assert_eq!(txt, "attributes=name");
576
577        let q = ScimEntryGetQuery {
578            attributes: Some(vec![Attribute::Name, Attribute::Spn]),
579            ext_access_check: true,
580            ..Default::default()
581        };
582
583        let txt = serde_urlencoded::to_string(&q).unwrap();
584        assert_eq!(txt, "attributes=name%2Cspn&extAccessCheck=true");
585    }
586
587    #[test]
588    fn test_scimfilter_attrname() {
589        assert_eq!(scimfilter::attrstring("abcd-_"), Ok("abcd-_".to_string()));
590        assert_eq!(scimfilter::attrstring("aB-_CD"), Ok("aB-_CD".to_string()));
591        assert_eq!(scimfilter::attrstring("a1-_23"), Ok("a1-_23".to_string()));
592        assert!(scimfilter::attrstring("-bcd").is_err());
593        assert!(scimfilter::attrstring("_bcd").is_err());
594        assert!(scimfilter::attrstring("0bcd").is_err());
595    }
596
597    #[test]
598    fn test_scimfilter_attrpath() {
599        assert_eq!(
600            scimfilter::attrpath("mail"),
601            Ok(AttrPath {
602                a: Attribute::from("mail"),
603                s: None
604            })
605        );
606
607        assert_eq!(
608            scimfilter::attrpath("mail.primary"),
609            Ok(AttrPath {
610                a: Attribute::from("mail"),
611                s: Some(SubAttribute::from("primary"))
612            })
613        );
614
615        assert!(scimfilter::attrname("mail.0").is_err());
616        assert!(scimfilter::attrname("mail._").is_err());
617        assert!(scimfilter::attrname("mail,0").is_err());
618        assert!(scimfilter::attrname(".primary").is_err());
619    }
620
621    #[test]
622    fn test_scimfilter_pres() {
623        assert!(
624            scimfilter::parse("mail pr")
625                == Ok(ScimFilter::Present(AttrPath {
626                    a: Attribute::from("mail"),
627                    s: None
628                }))
629        );
630    }
631
632    #[test]
633    fn test_scimfilter_eq() {
634        assert!(
635            scimfilter::parse("mail eq \"dcba\"")
636                == Ok(ScimFilter::Equal(
637                    AttrPath {
638                        a: Attribute::from("mail"),
639                        s: None
640                    },
641                    JsonValue::String("dcba".to_string())
642                ))
643        );
644    }
645
646    #[test]
647    fn test_scimfilter_ne() {
648        assert!(
649            scimfilter::parse("mail ne \"dcba\"")
650                == Ok(ScimFilter::NotEqual(
651                    AttrPath {
652                        a: Attribute::from("mail"),
653                        s: None
654                    },
655                    JsonValue::String("dcba".to_string())
656                ))
657        );
658    }
659
660    #[test]
661    fn test_scimfilter_co() {
662        assert!(
663            scimfilter::parse("mail co \"dcba\"")
664                == Ok(ScimFilter::Contains(
665                    AttrPath {
666                        a: Attribute::from("mail"),
667                        s: None
668                    },
669                    JsonValue::String("dcba".to_string())
670                ))
671        );
672    }
673
674    #[test]
675    fn test_scimfilter_sw() {
676        assert!(
677            scimfilter::parse("mail sw \"dcba\"")
678                == Ok(ScimFilter::StartsWith(
679                    AttrPath {
680                        a: Attribute::from("mail"),
681                        s: None
682                    },
683                    JsonValue::String("dcba".to_string())
684                ))
685        );
686    }
687
688    #[test]
689    fn test_scimfilter_ew() {
690        assert!(
691            scimfilter::parse("mail ew \"dcba\"")
692                == Ok(ScimFilter::EndsWith(
693                    AttrPath {
694                        a: Attribute::from("mail"),
695                        s: None
696                    },
697                    JsonValue::String("dcba".to_string())
698                ))
699        );
700    }
701
702    #[test]
703    fn test_scimfilter_gt() {
704        assert!(
705            scimfilter::parse("mail gt \"dcba\"")
706                == Ok(ScimFilter::Greater(
707                    AttrPath {
708                        a: Attribute::from("mail"),
709                        s: None
710                    },
711                    JsonValue::String("dcba".to_string())
712                ))
713        );
714    }
715
716    #[test]
717    fn test_scimfilter_lt() {
718        assert!(
719            scimfilter::parse("mail lt \"dcba\"")
720                == Ok(ScimFilter::Less(
721                    AttrPath {
722                        a: Attribute::from("mail"),
723                        s: None
724                    },
725                    JsonValue::String("dcba".to_string())
726                ))
727        );
728    }
729
730    #[test]
731    fn test_scimfilter_ge() {
732        assert!(
733            scimfilter::parse("mail ge \"dcba\"")
734                == Ok(ScimFilter::GreaterOrEqual(
735                    AttrPath {
736                        a: Attribute::from("mail"),
737                        s: None
738                    },
739                    JsonValue::String("dcba".to_string())
740                ))
741        );
742    }
743
744    #[test]
745    fn test_scimfilter_le() {
746        assert!(
747            scimfilter::parse("mail le \"dcba\"")
748                == Ok(ScimFilter::LessOrEqual(
749                    AttrPath {
750                        a: Attribute::from("mail"),
751                        s: None
752                    },
753                    JsonValue::String("dcba".to_string())
754                ))
755        );
756    }
757
758    #[test]
759    fn test_scimfilter_group() {
760        let f = scimfilter::parse("(mail eq \"dcba\")");
761        eprintln!("{f:?}");
762        assert!(
763            f == Ok(ScimFilter::Equal(
764                AttrPath {
765                    a: Attribute::from("mail"),
766                    s: None
767                },
768                JsonValue::String("dcba".to_string())
769            ))
770        );
771    }
772
773    #[test]
774    fn test_scimfilter_not() {
775        let f = scimfilter::parse("not (mail eq \"dcba\")");
776        eprintln!("{f:?}");
777
778        assert!(
779            f == Ok(ScimFilter::Not(Box::new(ScimFilter::Equal(
780                AttrPath {
781                    a: Attribute::from("mail"),
782                    s: None
783                },
784                JsonValue::String("dcba".to_string())
785            ))))
786        );
787    }
788
789    #[test]
790    fn test_scimfilter_and() {
791        let f = scimfilter::parse("mail eq \"dcba\" and name ne \"1234\"");
792        eprintln!("{f:?}");
793
794        assert!(
795            f == Ok(ScimFilter::And(
796                Box::new(ScimFilter::Equal(
797                    AttrPath {
798                        a: Attribute::from("mail"),
799                        s: None
800                    },
801                    JsonValue::String("dcba".to_string())
802                )),
803                Box::new(ScimFilter::NotEqual(
804                    AttrPath {
805                        a: Attribute::from("name"),
806                        s: None
807                    },
808                    JsonValue::String("1234".to_string())
809                ))
810            ))
811        );
812    }
813
814    #[test]
815    fn test_scimfilter_or() {
816        let f = scimfilter::parse("mail eq \"dcba\" or name ne \"1234\"");
817        eprintln!("{f:?}");
818
819        assert!(
820            f == Ok(ScimFilter::Or(
821                Box::new(ScimFilter::Equal(
822                    AttrPath {
823                        a: Attribute::from("mail"),
824                        s: None
825                    },
826                    JsonValue::String("dcba".to_string())
827                )),
828                Box::new(ScimFilter::NotEqual(
829                    AttrPath {
830                        a: Attribute::from("name"),
831                        s: None
832                    },
833                    JsonValue::String("1234".to_string())
834                ))
835            ))
836        );
837    }
838
839    #[test]
840    fn test_scimfilter_complex() {
841        let f = scimfilter::parse("mail[type eq \"work\"]");
842        eprintln!("-- {f:?}");
843        assert!(f.is_ok());
844
845        let f = scimfilter::parse("mail[type eq \"work\" and value co \"@example.com\"] or testattr[type eq \"xmpp\" and value co \"@foo.com\"]");
846        eprintln!("{f:?}");
847
848        assert_eq!(
849            f,
850            Ok(ScimFilter::Or(
851                Box::new(ScimFilter::Complex(
852                    Attribute::from("mail"),
853                    Box::new(ScimComplexFilter::And(
854                        Box::new(ScimComplexFilter::Equal(
855                            SubAttribute::from("type"),
856                            JsonValue::String("work".to_string())
857                        )),
858                        Box::new(ScimComplexFilter::Contains(
859                            SubAttribute::from("value"),
860                            JsonValue::String("@example.com".to_string())
861                        ))
862                    ))
863                )),
864                Box::new(ScimFilter::Complex(
865                    Attribute::from("testattr"),
866                    Box::new(ScimComplexFilter::And(
867                        Box::new(ScimComplexFilter::Equal(
868                            SubAttribute::from("type"),
869                            JsonValue::String("xmpp".to_string())
870                        )),
871                        Box::new(ScimComplexFilter::Contains(
872                            SubAttribute::from("value"),
873                            JsonValue::String("@foo.com".to_string())
874                        ))
875                    ))
876                ))
877            ))
878        );
879    }
880
881    #[test]
882    fn test_scimfilter_precedence_1() {
883        let f =
884            scimfilter::parse("testattr_a pr or testattr_b pr and testattr_c pr or testattr_d pr");
885        eprintln!("{f:?}");
886
887        assert!(
888            f == Ok(ScimFilter::Or(
889                Box::new(ScimFilter::Or(
890                    Box::new(ScimFilter::Present(AttrPath {
891                        a: Attribute::from("testattr_a"),
892                        s: None
893                    })),
894                    Box::new(ScimFilter::And(
895                        Box::new(ScimFilter::Present(AttrPath {
896                            a: Attribute::from("testattr_b"),
897                            s: None
898                        })),
899                        Box::new(ScimFilter::Present(AttrPath {
900                            a: Attribute::from("testattr_c"),
901                            s: None
902                        })),
903                    )),
904                )),
905                Box::new(ScimFilter::Present(AttrPath {
906                    a: Attribute::from("testattr_d"),
907                    s: None
908                }))
909            ))
910        );
911    }
912
913    #[test]
914    fn test_scimfilter_precedence_2() {
915        let f =
916            scimfilter::parse("testattr_a pr and testattr_b pr or testattr_c pr and testattr_d pr");
917        eprintln!("{f:?}");
918
919        assert!(
920            f == Ok(ScimFilter::Or(
921                Box::new(ScimFilter::And(
922                    Box::new(ScimFilter::Present(AttrPath {
923                        a: Attribute::from("testattr_a"),
924                        s: None
925                    })),
926                    Box::new(ScimFilter::Present(AttrPath {
927                        a: Attribute::from("testattr_b"),
928                        s: None
929                    })),
930                )),
931                Box::new(ScimFilter::And(
932                    Box::new(ScimFilter::Present(AttrPath {
933                        a: Attribute::from("testattr_c"),
934                        s: None
935                    })),
936                    Box::new(ScimFilter::Present(AttrPath {
937                        a: Attribute::from("testattr_d"),
938                        s: None
939                    })),
940                )),
941            ))
942        );
943    }
944
945    #[test]
946    fn test_scimfilter_precedence_3() {
947        let f = scimfilter::parse(
948            "testattr_a pr and (testattr_b pr or testattr_c pr) and testattr_d pr",
949        );
950        eprintln!("{f:?}");
951
952        assert!(
953            f == Ok(ScimFilter::And(
954                Box::new(ScimFilter::And(
955                    Box::new(ScimFilter::Present(AttrPath {
956                        a: Attribute::from("testattr_a"),
957                        s: None
958                    })),
959                    Box::new(ScimFilter::Or(
960                        Box::new(ScimFilter::Present(AttrPath {
961                            a: Attribute::from("testattr_b"),
962                            s: None
963                        })),
964                        Box::new(ScimFilter::Present(AttrPath {
965                            a: Attribute::from("testattr_c"),
966                            s: None
967                        })),
968                    )),
969                )),
970                Box::new(ScimFilter::Present(AttrPath {
971                    a: Attribute::from("testattr_d"),
972                    s: None
973                })),
974            ))
975        );
976    }
977
978    #[test]
979    fn test_scimfilter_precedence_4() {
980        let f = scimfilter::parse(
981            "testattr_a pr and not (testattr_b pr or testattr_c pr) and testattr_d pr",
982        );
983        eprintln!("{f:?}");
984
985        assert!(
986            f == Ok(ScimFilter::And(
987                Box::new(ScimFilter::And(
988                    Box::new(ScimFilter::Present(AttrPath {
989                        a: Attribute::from("testattr_a"),
990                        s: None
991                    })),
992                    Box::new(ScimFilter::Not(Box::new(ScimFilter::Or(
993                        Box::new(ScimFilter::Present(AttrPath {
994                            a: Attribute::from("testattr_b"),
995                            s: None
996                        })),
997                        Box::new(ScimFilter::Present(AttrPath {
998                            a: Attribute::from("testattr_c"),
999                            s: None
1000                        })),
1001                    )))),
1002                )),
1003                Box::new(ScimFilter::Present(AttrPath {
1004                    a: Attribute::from("testattr_d"),
1005                    s: None
1006                })),
1007            ))
1008        );
1009    }
1010
1011    #[test]
1012    fn test_scimfilter_quoted_values() {
1013        assert_eq!(
1014            scimfilter::parse(r#"description eq "text ( ) [ ] 'single' \"escaped\" \\\\consecutive\\\\ \/slash\b\f\n\r\t\u0041 and or not eq ne co sw ew gt lt ge le pr true false""#),
1015            Ok(ScimFilter::Equal(
1016                AttrPath { a: Attribute::from("description"), s: None },
1017                JsonValue::String("text ( ) [ ] 'single' \"escaped\" \\\\consecutive\\\\ /slash\u{08}\u{0C}\n\r\tA and or not eq ne co sw ew gt lt ge le pr true false".to_string())
1018            ))
1019        );
1020    }
1021
1022    #[test]
1023    fn test_scimfilter_quoted_values_incomplete_escape() {
1024        let result = scimfilter::parse(r#"name eq "test\""#);
1025        assert!(result.is_err());
1026    }
1027
1028    #[test]
1029    fn test_scimfilter_quoted_values_empty() {
1030        assert_eq!(
1031            scimfilter::parse(r#"name eq """#),
1032            Ok(ScimFilter::Equal(
1033                AttrPath {
1034                    a: Attribute::from("name"),
1035                    s: None
1036                },
1037                JsonValue::String("".to_string())
1038            ))
1039        );
1040    }
1041
1042    #[test]
1043    fn test_scimfilter_recursion_limit() {
1044        scimfilter::parse_depth("name pr and (name pr and (name pr and name pr))", 0)
1045            .expect_err("Must fail");
1046
1047        scimfilter::parse_depth("name pr and (name pr and (name pr and name pr))", 1)
1048            .expect_err("Must fail");
1049
1050        scimfilter::parse_depth("name pr and (name pr and (name pr and name pr))", 2)
1051            .expect_err("Must fail");
1052
1053        scimfilter::parse_depth("name pr and (name pr and (name pr and name pr))", 3)
1054            .expect("Must pass");
1055    }
1056}