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