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