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