1use 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#[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#[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 #[serde(default)]
72 pub sort_by: Option<Attribute>,
73 #[serde(default)]
74 pub sort_order: Option<ScimSortOrder>,
75
76 #[schema(value_type = u64)]
78 pub start_index: Option<NonZeroU64>,
79 #[schema(value_type = u64)]
80 pub count: Option<NonZeroU64>,
81
82 #[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 }
465
466 #[test]
467 fn scim_kani_to_generic() {
468 }
470
471 #[test]
472 fn scim_kani_to_rfc() {
473 }
475
476 #[test]
477 fn scim_sync_kani_to_rfc() {
478 use super::*;
479
480 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 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}