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)]
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)]
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 pub start_index: Option<NonZeroU64>,
78 pub count: Option<NonZeroU64>,
79
80 #[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 }
460
461 #[test]
462 fn scim_kani_to_generic() {
463 }
465
466 #[test]
467 fn scim_kani_to_rfc() {
468 }
470
471 #[test]
472 fn scim_sync_kani_to_rfc() {
473 use super::*;
474
475 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 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}