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