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            limiter(max_depth) a:parse_inner(max_depth.saturating_sub(1)) { a }
273
274        rule limiter(max_depth: usize) -> () =
275            {? if max_depth == 0 { Err("too deeply nested") } else { Ok(()) } }
276
277        rule parse_inner(max_depth: usize) -> ScimFilter = precedence!{
278            a:(@) separator()+ "or" separator()+ b:@ {
279                ScimFilter::Or(
280                    Box::new(a),
281                    Box::new(b)
282                )
283            }
284            --
285            a:(@) separator()+ "and" separator()+ b:@ {
286                ScimFilter::And(
287                    Box::new(a),
288                    Box::new(b)
289                )
290            }
291            --
292            "not" separator()+ "(" e:parse_depth(max_depth) ")" {
293                ScimFilter::Not(Box::new(e))
294            }
295            --
296            a:attrname()"[" e:parse_complex_depth(max_depth) "]" {
297                ScimFilter::Complex(
298                    a,
299                    Box::new(e)
300                )
301            }
302            --
303            a:attrexp() { a }
304            "(" e:parse_depth(max_depth) ")" { e }
305        }
306
307        pub rule parse_complex() -> ScimComplexFilter =
308            f:parse_complex_depth(SCIM_FILTER_MAX_DEPTH) { f }
309
310        pub(crate) rule parse_complex_depth(max_depth: usize) -> ScimComplexFilter =
311            limiter(max_depth) a:parse_complex_inner(max_depth.saturating_sub(1)) { a }
312
313        rule parse_complex_inner(max_depth: usize) -> ScimComplexFilter = precedence!{
314            a:(@) separator()+ "or" separator()+ b:@ {
315                ScimComplexFilter::Or(
316                    Box::new(a),
317                    Box::new(b)
318                )
319            }
320            --
321            a:(@) separator()+ "and" separator()+ b:@ {
322                ScimComplexFilter::And(
323                    Box::new(a),
324                    Box::new(b)
325                )
326            }
327            --
328            "not" separator()+ "(" e:parse_complex_depth(max_depth) ")" {
329                ScimComplexFilter::Not(Box::new(e))
330            }
331            --
332            a:complex_attrexp() { a }
333            "(" e:parse_complex_depth(max_depth) ")" { e }
334        }
335
336        pub(crate) rule attrexp() -> ScimFilter =
337            pres()
338            / eq()
339            / ne()
340            / co()
341            / sw()
342            / ew()
343            / gt()
344            / lt()
345            / ge()
346            / le()
347
348        pub(crate) rule pres() -> ScimFilter =
349            a:attrpath() separator()+ "pr" { ScimFilter::Present(a) }
350
351        pub(crate) rule eq() -> ScimFilter =
352            a:attrpath() separator()+ "eq" separator()+ v:value() { ScimFilter::Equal(a, v) }
353
354        pub(crate) rule ne() -> ScimFilter =
355            a:attrpath() separator()+ "ne" separator()+ v:value() { ScimFilter::NotEqual(a, v) }
356
357        pub(crate) rule co() -> ScimFilter =
358            a:attrpath() separator()+ "co" separator()+ v:value() { ScimFilter::Contains(a, v) }
359
360        pub(crate) rule sw() -> ScimFilter =
361            a:attrpath() separator()+ "sw" separator()+ v:value() { ScimFilter::StartsWith(a, v) }
362
363        pub(crate) rule ew() -> ScimFilter =
364            a:attrpath() separator()+ "ew" separator()+ v:value() { ScimFilter::EndsWith(a, v) }
365
366        pub(crate) rule gt() -> ScimFilter =
367            a:attrpath() separator()+ "gt" separator()+ v:value() { ScimFilter::Greater(a, v) }
368
369        pub(crate) rule lt() -> ScimFilter =
370            a:attrpath() separator()+ "lt" separator()+ v:value() { ScimFilter::Less(a, v) }
371
372        pub(crate) rule ge() -> ScimFilter =
373            a:attrpath() separator()+ "ge" separator()+ v:value() { ScimFilter::GreaterOrEqual(a, v) }
374
375        pub(crate) rule le() -> ScimFilter =
376            a:attrpath() separator()+ "le" separator()+ v:value() { ScimFilter::LessOrEqual(a, v) }
377
378        pub(crate) rule complex_attrexp() -> ScimComplexFilter =
379            c_pres()
380            / c_eq()
381            / c_ne()
382            / c_co()
383            / c_sw()
384            / c_ew()
385            / c_gt()
386            / c_lt()
387            / c_ge()
388            / c_le()
389
390        pub(crate) rule c_pres() -> ScimComplexFilter =
391            a:subattr() separator()+ "pr" { ScimComplexFilter::Present(a) }
392
393        pub(crate) rule c_eq() -> ScimComplexFilter =
394            a:subattr() separator()+ "eq" separator()+ v:value() { ScimComplexFilter::Equal(a, v) }
395
396        pub(crate) rule c_ne() -> ScimComplexFilter =
397            a:subattr() separator()+ "ne" separator()+ v:value() { ScimComplexFilter::NotEqual(a, v) }
398
399        pub(crate) rule c_co() -> ScimComplexFilter =
400            a:subattr() separator()+ "co" separator()+ v:value() { ScimComplexFilter::Contains(a, v) }
401
402        pub(crate) rule c_sw() -> ScimComplexFilter =
403            a:subattr() separator()+ "sw" separator()+ v:value() { ScimComplexFilter::StartsWith(a, v) }
404
405        pub(crate) rule c_ew() -> ScimComplexFilter =
406            a:subattr() separator()+ "ew" separator()+ v:value() { ScimComplexFilter::EndsWith(a, v) }
407
408        pub(crate) rule c_gt() -> ScimComplexFilter =
409            a:subattr() separator()+ "gt" separator()+ v:value() { ScimComplexFilter::Greater(a, v) }
410
411        pub(crate) rule c_lt() -> ScimComplexFilter =
412            a:subattr() separator()+ "lt" separator()+ v:value() { ScimComplexFilter::Less(a, v) }
413
414        pub(crate) rule c_ge() -> ScimComplexFilter =
415            a:subattr() separator()+ "ge" separator()+ v:value() { ScimComplexFilter::GreaterOrEqual(a, v) }
416
417        pub(crate) rule c_le() -> ScimComplexFilter =
418            a:subattr() separator()+ "le" separator()+ v:value() { ScimComplexFilter::LessOrEqual(a, v) }
419
420        rule separator() =
421            ['\n' | ' ' | '\t' ]
422
423        rule operator() =
424            ['\n' | ' ' | '\t' | '(' | ')' | '[' | ']' ]
425
426        rule value() -> JsonValue =
427            quotedvalue() / unquotedvalue()
428
429        rule quotedvalue() -> JsonValue =
430            s:$(['"'] ((['\\'][_]) / (!['"'][_]))* ['"']) {? serde_json::from_str(s).map_err(|_| "invalid json value" ) }
431
432        rule unquotedvalue() -> JsonValue =
433            s:$((!operator()[_])*) {? serde_json::from_str(s).map_err(|_| "invalid json value" ) }
434
435        pub(crate) rule attrpath() -> AttrPath =
436            a:attrname() s:dot_subattr()? { AttrPath { a, s } }
437
438        rule dot_subattr() -> SubAttribute =
439            "." s:subattr() { s }
440
441        rule subattr() -> SubAttribute =
442            s:attrstring() { SubAttribute::from(s.as_str()) }
443
444        pub(crate) rule attrname() -> Attribute =
445            s:attrstring() { Attribute::from(s.as_str()) }
446
447        pub(crate) rule attrstring() -> String =
448            s:$([ 'a'..='z' | 'A'..='Z']['a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' ]*) { s.to_string() }
449    }
450}
451
452impl FromStr for AttrPath {
453    type Err = peg::error::ParseError<peg::str::LineCol>;
454    fn from_str(input: &str) -> Result<Self, Self::Err> {
455        scimfilter::attrpath(input)
456    }
457}
458
459impl FromStr for ScimFilter {
460    type Err = peg::error::ParseError<peg::str::LineCol>;
461    fn from_str(input: &str) -> Result<Self, Self::Err> {
462        scimfilter::parse(input)
463    }
464}
465
466impl FromStr for ScimComplexFilter {
467    type Err = peg::error::ParseError<peg::str::LineCol>;
468    fn from_str(input: &str) -> Result<Self, Self::Err> {
469        scimfilter::parse_complex(input)
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476
477    #[test]
478    fn scim_rfc_to_generic() {
479        // Assert that we can transition from the rfc generic entries to the
480        // kanidm types.
481    }
482
483    #[test]
484    fn scim_kani_to_generic() {
485        // Assert that a kanidm strong entry can convert to generic.
486    }
487
488    #[test]
489    fn scim_kani_to_rfc() {
490        // Assert that a kanidm strong entry can convert to rfc.
491    }
492
493    #[test]
494    fn scim_sync_kani_to_rfc() {
495        use super::*;
496
497        // Group
498        let group_uuid = uuid::uuid!("2d0a9e7c-cc08-4ca2-8d7f-114f9abcfc8a");
499
500        let group = ScimSyncGroup::builder(
501            group_uuid,
502            "cn=testgroup".to_string(),
503            "testgroup".to_string(),
504        )
505        .set_description(Some("test desc".to_string()))
506        .set_gidnumber(Some(12345))
507        .set_members(vec!["member_a".to_string(), "member_a".to_string()].into_iter())
508        .build();
509
510        let entry: Result<ScimEntry, _> = group.try_into();
511
512        assert!(entry.is_ok());
513
514        // User
515        let user_uuid = uuid::uuid!("cb3de098-33fd-4565-9d80-4f7ed6a664e9");
516
517        let user_sshkey = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBENubZikrb8hu+HeVRdZ0pp/VAk2qv4JDbuJhvD0yNdWDL2e3cBbERiDeNPkWx58Q4rVnxkbV1fa8E2waRtT91wAAAAEc3NoOg== testuser@fidokey";
518
519        let person = ScimSyncPerson::builder(
520            user_uuid,
521            "cn=testuser".to_string(),
522            "testuser".to_string(),
523            "Test User".to_string(),
524        )
525        .set_password_import(Some("new_password".to_string()))
526        .set_unix_password_import(Some("new_password".to_string()))
527        .set_totp_import(vec![ScimTotp {
528            external_id: "Totp".to_string(),
529            secret: "abcd".to_string(),
530            algo: "SHA3".to_string(),
531            step: 60,
532            digits: 8,
533        }])
534        .set_mail(vec![MultiValueAttr {
535            primary: Some(true),
536            value: "testuser@example.com".to_string(),
537            ..Default::default()
538        }])
539        .set_ssh_publickey(vec![ScimSshPubKey {
540            label: "Key McKeyface".to_string(),
541            value: user_sshkey.to_string(),
542        }])
543        .set_login_shell(Some("/bin/false".to_string()))
544        .set_account_valid_from(Some("2023-11-28T04:57:55Z".to_string()))
545        .set_account_expire(Some("2023-11-28T04:57:55Z".to_string()))
546        .set_gidnumber(Some(54321))
547        .build();
548
549        let entry: Result<ScimEntry, _> = person.try_into();
550
551        assert!(entry.is_ok());
552    }
553
554    #[test]
555    fn scim_entry_get_query() {
556        use super::*;
557
558        let q = ScimEntryGetQuery {
559            attributes: None,
560            ..Default::default()
561        };
562
563        let txt = serde_urlencoded::to_string(&q).unwrap();
564
565        assert_eq!(txt, "");
566
567        let q = ScimEntryGetQuery {
568            attributes: Some(vec![Attribute::Name]),
569            ext_access_check: false,
570            ..Default::default()
571        };
572
573        let txt = serde_urlencoded::to_string(&q).unwrap();
574        assert_eq!(txt, "attributes=name");
575
576        let q = ScimEntryGetQuery {
577            attributes: Some(vec![Attribute::Name, Attribute::Spn]),
578            ext_access_check: true,
579            ..Default::default()
580        };
581
582        let txt = serde_urlencoded::to_string(&q).unwrap();
583        assert_eq!(txt, "attributes=name%2Cspn&extAccessCheck=true");
584    }
585
586    #[test]
587    fn test_scimfilter_attrname() {
588        assert_eq!(scimfilter::attrstring("abcd-_"), Ok("abcd-_".to_string()));
589        assert_eq!(scimfilter::attrstring("aB-_CD"), Ok("aB-_CD".to_string()));
590        assert_eq!(scimfilter::attrstring("a1-_23"), Ok("a1-_23".to_string()));
591        assert!(scimfilter::attrstring("-bcd").is_err());
592        assert!(scimfilter::attrstring("_bcd").is_err());
593        assert!(scimfilter::attrstring("0bcd").is_err());
594    }
595
596    #[test]
597    fn test_scimfilter_attrpath() {
598        assert_eq!(
599            scimfilter::attrpath("mail"),
600            Ok(AttrPath {
601                a: Attribute::from("mail"),
602                s: None
603            })
604        );
605
606        assert_eq!(
607            scimfilter::attrpath("mail.primary"),
608            Ok(AttrPath {
609                a: Attribute::from("mail"),
610                s: Some(SubAttribute::from("primary"))
611            })
612        );
613
614        assert!(scimfilter::attrname("mail.0").is_err());
615        assert!(scimfilter::attrname("mail._").is_err());
616        assert!(scimfilter::attrname("mail,0").is_err());
617        assert!(scimfilter::attrname(".primary").is_err());
618    }
619
620    #[test]
621    fn test_scimfilter_pres() {
622        assert!(
623            scimfilter::parse("mail pr")
624                == Ok(ScimFilter::Present(AttrPath {
625                    a: Attribute::from("mail"),
626                    s: None
627                }))
628        );
629    }
630
631    #[test]
632    fn test_scimfilter_eq() {
633        assert!(
634            scimfilter::parse("mail eq \"dcba\"")
635                == Ok(ScimFilter::Equal(
636                    AttrPath {
637                        a: Attribute::from("mail"),
638                        s: None
639                    },
640                    JsonValue::String("dcba".to_string())
641                ))
642        );
643    }
644
645    #[test]
646    fn test_scimfilter_ne() {
647        assert!(
648            scimfilter::parse("mail ne \"dcba\"")
649                == Ok(ScimFilter::NotEqual(
650                    AttrPath {
651                        a: Attribute::from("mail"),
652                        s: None
653                    },
654                    JsonValue::String("dcba".to_string())
655                ))
656        );
657    }
658
659    #[test]
660    fn test_scimfilter_co() {
661        assert!(
662            scimfilter::parse("mail co \"dcba\"")
663                == Ok(ScimFilter::Contains(
664                    AttrPath {
665                        a: Attribute::from("mail"),
666                        s: None
667                    },
668                    JsonValue::String("dcba".to_string())
669                ))
670        );
671    }
672
673    #[test]
674    fn test_scimfilter_sw() {
675        assert!(
676            scimfilter::parse("mail sw \"dcba\"")
677                == Ok(ScimFilter::StartsWith(
678                    AttrPath {
679                        a: Attribute::from("mail"),
680                        s: None
681                    },
682                    JsonValue::String("dcba".to_string())
683                ))
684        );
685    }
686
687    #[test]
688    fn test_scimfilter_ew() {
689        assert!(
690            scimfilter::parse("mail ew \"dcba\"")
691                == Ok(ScimFilter::EndsWith(
692                    AttrPath {
693                        a: Attribute::from("mail"),
694                        s: None
695                    },
696                    JsonValue::String("dcba".to_string())
697                ))
698        );
699    }
700
701    #[test]
702    fn test_scimfilter_gt() {
703        assert!(
704            scimfilter::parse("mail gt \"dcba\"")
705                == Ok(ScimFilter::Greater(
706                    AttrPath {
707                        a: Attribute::from("mail"),
708                        s: None
709                    },
710                    JsonValue::String("dcba".to_string())
711                ))
712        );
713    }
714
715    #[test]
716    fn test_scimfilter_lt() {
717        assert!(
718            scimfilter::parse("mail lt \"dcba\"")
719                == Ok(ScimFilter::Less(
720                    AttrPath {
721                        a: Attribute::from("mail"),
722                        s: None
723                    },
724                    JsonValue::String("dcba".to_string())
725                ))
726        );
727    }
728
729    #[test]
730    fn test_scimfilter_ge() {
731        assert!(
732            scimfilter::parse("mail ge \"dcba\"")
733                == Ok(ScimFilter::GreaterOrEqual(
734                    AttrPath {
735                        a: Attribute::from("mail"),
736                        s: None
737                    },
738                    JsonValue::String("dcba".to_string())
739                ))
740        );
741    }
742
743    #[test]
744    fn test_scimfilter_le() {
745        assert!(
746            scimfilter::parse("mail le \"dcba\"")
747                == Ok(ScimFilter::LessOrEqual(
748                    AttrPath {
749                        a: Attribute::from("mail"),
750                        s: None
751                    },
752                    JsonValue::String("dcba".to_string())
753                ))
754        );
755    }
756
757    #[test]
758    fn test_scimfilter_group() {
759        let f = scimfilter::parse("(mail eq \"dcba\")");
760        eprintln!("{f:?}");
761        assert!(
762            f == Ok(ScimFilter::Equal(
763                AttrPath {
764                    a: Attribute::from("mail"),
765                    s: None
766                },
767                JsonValue::String("dcba".to_string())
768            ))
769        );
770    }
771
772    #[test]
773    fn test_scimfilter_not() {
774        let f = scimfilter::parse("not (mail eq \"dcba\")");
775        eprintln!("{f:?}");
776
777        assert!(
778            f == Ok(ScimFilter::Not(Box::new(ScimFilter::Equal(
779                AttrPath {
780                    a: Attribute::from("mail"),
781                    s: None
782                },
783                JsonValue::String("dcba".to_string())
784            ))))
785        );
786    }
787
788    #[test]
789    fn test_scimfilter_and() {
790        let f = scimfilter::parse("mail eq \"dcba\" and name ne \"1234\"");
791        eprintln!("{f:?}");
792
793        assert!(
794            f == Ok(ScimFilter::And(
795                Box::new(ScimFilter::Equal(
796                    AttrPath {
797                        a: Attribute::from("mail"),
798                        s: None
799                    },
800                    JsonValue::String("dcba".to_string())
801                )),
802                Box::new(ScimFilter::NotEqual(
803                    AttrPath {
804                        a: Attribute::from("name"),
805                        s: None
806                    },
807                    JsonValue::String("1234".to_string())
808                ))
809            ))
810        );
811    }
812
813    #[test]
814    fn test_scimfilter_or() {
815        let f = scimfilter::parse("mail eq \"dcba\" or name ne \"1234\"");
816        eprintln!("{f:?}");
817
818        assert!(
819            f == Ok(ScimFilter::Or(
820                Box::new(ScimFilter::Equal(
821                    AttrPath {
822                        a: Attribute::from("mail"),
823                        s: None
824                    },
825                    JsonValue::String("dcba".to_string())
826                )),
827                Box::new(ScimFilter::NotEqual(
828                    AttrPath {
829                        a: Attribute::from("name"),
830                        s: None
831                    },
832                    JsonValue::String("1234".to_string())
833                ))
834            ))
835        );
836    }
837
838    #[test]
839    fn test_scimfilter_complex() {
840        let f = scimfilter::parse("mail[type eq \"work\"]");
841        eprintln!("-- {f:?}");
842        assert!(f.is_ok());
843
844        let f = scimfilter::parse("mail[type eq \"work\" and value co \"@example.com\"] or testattr[type eq \"xmpp\" and value co \"@foo.com\"]");
845        eprintln!("{f:?}");
846
847        assert_eq!(
848            f,
849            Ok(ScimFilter::Or(
850                Box::new(ScimFilter::Complex(
851                    Attribute::from("mail"),
852                    Box::new(ScimComplexFilter::And(
853                        Box::new(ScimComplexFilter::Equal(
854                            SubAttribute::from("type"),
855                            JsonValue::String("work".to_string())
856                        )),
857                        Box::new(ScimComplexFilter::Contains(
858                            SubAttribute::from("value"),
859                            JsonValue::String("@example.com".to_string())
860                        ))
861                    ))
862                )),
863                Box::new(ScimFilter::Complex(
864                    Attribute::from("testattr"),
865                    Box::new(ScimComplexFilter::And(
866                        Box::new(ScimComplexFilter::Equal(
867                            SubAttribute::from("type"),
868                            JsonValue::String("xmpp".to_string())
869                        )),
870                        Box::new(ScimComplexFilter::Contains(
871                            SubAttribute::from("value"),
872                            JsonValue::String("@foo.com".to_string())
873                        ))
874                    ))
875                ))
876            ))
877        );
878    }
879
880    #[test]
881    fn test_scimfilter_precedence_1() {
882        let f =
883            scimfilter::parse("testattr_a pr or testattr_b pr and testattr_c pr or testattr_d pr");
884        eprintln!("{f:?}");
885
886        assert!(
887            f == Ok(ScimFilter::Or(
888                Box::new(ScimFilter::Or(
889                    Box::new(ScimFilter::Present(AttrPath {
890                        a: Attribute::from("testattr_a"),
891                        s: None
892                    })),
893                    Box::new(ScimFilter::And(
894                        Box::new(ScimFilter::Present(AttrPath {
895                            a: Attribute::from("testattr_b"),
896                            s: None
897                        })),
898                        Box::new(ScimFilter::Present(AttrPath {
899                            a: Attribute::from("testattr_c"),
900                            s: None
901                        })),
902                    )),
903                )),
904                Box::new(ScimFilter::Present(AttrPath {
905                    a: Attribute::from("testattr_d"),
906                    s: None
907                }))
908            ))
909        );
910    }
911
912    #[test]
913    fn test_scimfilter_precedence_2() {
914        let f =
915            scimfilter::parse("testattr_a pr and testattr_b pr or testattr_c pr and testattr_d pr");
916        eprintln!("{f:?}");
917
918        assert!(
919            f == Ok(ScimFilter::Or(
920                Box::new(ScimFilter::And(
921                    Box::new(ScimFilter::Present(AttrPath {
922                        a: Attribute::from("testattr_a"),
923                        s: None
924                    })),
925                    Box::new(ScimFilter::Present(AttrPath {
926                        a: Attribute::from("testattr_b"),
927                        s: None
928                    })),
929                )),
930                Box::new(ScimFilter::And(
931                    Box::new(ScimFilter::Present(AttrPath {
932                        a: Attribute::from("testattr_c"),
933                        s: None
934                    })),
935                    Box::new(ScimFilter::Present(AttrPath {
936                        a: Attribute::from("testattr_d"),
937                        s: None
938                    })),
939                )),
940            ))
941        );
942    }
943
944    #[test]
945    fn test_scimfilter_precedence_3() {
946        let f = scimfilter::parse(
947            "testattr_a pr and (testattr_b pr or testattr_c pr) and testattr_d pr",
948        );
949        eprintln!("{f:?}");
950
951        assert!(
952            f == Ok(ScimFilter::And(
953                Box::new(ScimFilter::And(
954                    Box::new(ScimFilter::Present(AttrPath {
955                        a: Attribute::from("testattr_a"),
956                        s: None
957                    })),
958                    Box::new(ScimFilter::Or(
959                        Box::new(ScimFilter::Present(AttrPath {
960                            a: Attribute::from("testattr_b"),
961                            s: None
962                        })),
963                        Box::new(ScimFilter::Present(AttrPath {
964                            a: Attribute::from("testattr_c"),
965                            s: None
966                        })),
967                    )),
968                )),
969                Box::new(ScimFilter::Present(AttrPath {
970                    a: Attribute::from("testattr_d"),
971                    s: None
972                })),
973            ))
974        );
975    }
976
977    #[test]
978    fn test_scimfilter_precedence_4() {
979        let f = scimfilter::parse(
980            "testattr_a pr and not (testattr_b pr or testattr_c pr) and testattr_d pr",
981        );
982        eprintln!("{f:?}");
983
984        assert!(
985            f == Ok(ScimFilter::And(
986                Box::new(ScimFilter::And(
987                    Box::new(ScimFilter::Present(AttrPath {
988                        a: Attribute::from("testattr_a"),
989                        s: None
990                    })),
991                    Box::new(ScimFilter::Not(Box::new(ScimFilter::Or(
992                        Box::new(ScimFilter::Present(AttrPath {
993                            a: Attribute::from("testattr_b"),
994                            s: None
995                        })),
996                        Box::new(ScimFilter::Present(AttrPath {
997                            a: Attribute::from("testattr_c"),
998                            s: None
999                        })),
1000                    )))),
1001                )),
1002                Box::new(ScimFilter::Present(AttrPath {
1003                    a: Attribute::from("testattr_d"),
1004                    s: None
1005                })),
1006            ))
1007        );
1008    }
1009
1010    #[test]
1011    fn test_scimfilter_quoted_values() {
1012        assert_eq!(
1013            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""#),
1014            Ok(ScimFilter::Equal(
1015                AttrPath { a: Attribute::from("description"), s: None },
1016                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())
1017            ))
1018        );
1019    }
1020
1021    #[test]
1022    fn test_scimfilter_quoted_values_incomplete_escape() {
1023        let result = scimfilter::parse(r#"name eq "test\""#);
1024        assert!(result.is_err());
1025    }
1026
1027    #[test]
1028    fn test_scimfilter_quoted_values_empty() {
1029        assert_eq!(
1030            scimfilter::parse(r#"name eq """#),
1031            Ok(ScimFilter::Equal(
1032                AttrPath {
1033                    a: Attribute::from("name"),
1034                    s: None
1035                },
1036                JsonValue::String("".to_string())
1037            ))
1038        );
1039    }
1040
1041    #[test]
1042    fn test_scimfilter_recursion_limit() {
1043        scimfilter::parse_depth("name pr and (name pr and (name pr and name pr))", 0)
1044            .expect_err("Must fail");
1045
1046        scimfilter::parse_depth("name pr and (name pr and (name pr and name pr))", 1)
1047            .expect_err("Must fail");
1048
1049        scimfilter::parse_depth("name pr and (name pr and (name pr and name pr))", 2)
1050            .expect_err("Must fail");
1051
1052        scimfilter::parse_depth("name pr and (name pr and (name pr and name pr))", 3)
1053            .expect("Must pass");
1054
1055        scimfilter::parse_depth("((name pr and name pr))", 0).expect_err("Must fail");
1056
1057        scimfilter::parse_depth("((name pr and name pr))", 1).expect_err("Must fail");
1058
1059        scimfilter::parse_depth("((name pr and name pr))", 2).expect_err("Must fail");
1060
1061        scimfilter::parse_depth("((name pr and name pr))", 3).expect("Must pass");
1062    }
1063}