scim_proto/
lib.rs

1#![deny(warnings)]
2#![warn(unused_extern_crates)]
3#![deny(clippy::todo)]
4#![deny(clippy::unimplemented)]
5#![deny(clippy::unwrap_used)]
6#![deny(clippy::expect_used)]
7#![deny(clippy::panic)]
8#![deny(clippy::unreachable)]
9#![deny(clippy::await_holding_lock)]
10#![deny(clippy::needless_pass_by_value)]
11#![deny(clippy::trivially_copy_pass_by_ref)]
12
13use base64urlsafedata::Base64UrlSafeData;
14use serde::{Deserialize, Serialize};
15use std::collections::BTreeMap;
16use time::{format_description::well_known::Rfc3339, OffsetDateTime};
17use url::Url;
18use utoipa::ToSchema;
19use uuid::Uuid;
20
21pub mod constants;
22pub mod filter;
23pub mod group;
24pub mod user;
25
26pub mod prelude {
27    pub use crate::constants::*;
28    pub use crate::user::MultiValueAttr;
29    pub use crate::{ScimAttr, ScimComplexAttr, ScimEntry, ScimEntryHeader, ScimMeta, ScimValue};
30}
31
32#[derive(Deserialize, Serialize, Debug, Clone, ToSchema)]
33#[serde(untagged)]
34pub enum ScimAttr {
35    Bool(bool),
36    Integer(i64),
37    Decimal(f64),
38    String(String),
39    // These can't be implicitly decoded because we may not know the intent, but we can *encode* them.
40    // That's why "String" is above this because it catches anything during deserialization before
41    // this point.
42    #[serde(with = "time::serde::rfc3339")]
43    DateTime(OffsetDateTime),
44
45    Binary(Base64UrlSafeData),
46    Reference(Url),
47}
48
49impl ScimAttr {
50    pub fn parse_as_datetime(&self) -> Option<Self> {
51        let s = match self {
52            ScimAttr::String(s) => s,
53            _ => return None,
54        };
55
56        OffsetDateTime::parse(s, &Rfc3339)
57            .map(ScimAttr::DateTime)
58            .ok()
59    }
60}
61
62impl From<String> for ScimAttr {
63    fn from(s: String) -> Self {
64        ScimAttr::String(s)
65    }
66}
67
68impl From<bool> for ScimAttr {
69    fn from(b: bool) -> Self {
70        ScimAttr::Bool(b)
71    }
72}
73
74impl From<u32> for ScimAttr {
75    fn from(i: u32) -> Self {
76        ScimAttr::Integer(i as i64)
77    }
78}
79
80impl From<Vec<u8>> for ScimAttr {
81    fn from(data: Vec<u8>) -> Self {
82        ScimAttr::Binary(data.into())
83    }
84}
85
86impl From<OffsetDateTime> for ScimAttr {
87    fn from(odt: OffsetDateTime) -> Self {
88        ScimAttr::DateTime(odt)
89    }
90}
91
92impl From<ScimAttr> for ScimValue {
93    fn from(sa: ScimAttr) -> Self {
94        ScimValue::Simple(sa)
95    }
96}
97
98impl Eq for ScimAttr {}
99
100impl PartialEq for ScimAttr {
101    fn eq(&self, other: &Self) -> bool {
102        match (self, other) {
103            (ScimAttr::String(l), ScimAttr::String(r)) => l == r,
104            (ScimAttr::Bool(l), ScimAttr::Bool(r)) => l == r,
105            (ScimAttr::Decimal(l), ScimAttr::Decimal(r)) => l == r,
106            (ScimAttr::Integer(l), ScimAttr::Integer(r)) => l == r,
107            (ScimAttr::DateTime(l), ScimAttr::DateTime(r)) => l == r,
108            (ScimAttr::Binary(l), ScimAttr::Binary(r)) => l == r,
109            (ScimAttr::Reference(l), ScimAttr::Reference(r)) => l == r,
110            _ => false,
111        }
112    }
113}
114
115pub type ScimComplexAttr = BTreeMap<String, ScimAttr>;
116
117#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema)]
118#[serde(untagged)]
119pub enum ScimValue {
120    Simple(ScimAttr),
121    Complex(ScimComplexAttr),
122    MultiSimple(Vec<ScimAttr>),
123    MultiComplex(Vec<ScimComplexAttr>),
124}
125
126impl ScimValue {
127    pub fn len(&self) -> usize {
128        match self {
129            ScimValue::Simple(_) | ScimValue::Complex(_) => 1,
130            ScimValue::MultiSimple(a) => a.len(),
131            ScimValue::MultiComplex(a) => a.len(),
132        }
133    }
134
135    pub fn is_empty(&self) -> bool {
136        self.len() == 0
137    }
138}
139
140#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema)]
141#[serde(rename_all = "camelCase", deny_unknown_fields)]
142pub struct ScimMeta {
143    pub resource_type: String,
144    #[serde(with = "time::serde::rfc3339")]
145    pub created: OffsetDateTime,
146    #[serde(with = "time::serde::rfc3339")]
147    pub last_modified: OffsetDateTime,
148    pub location: Url,
149    pub version: String,
150}
151
152#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema)]
153#[serde(rename_all = "camelCase")]
154pub struct ScimEntryHeader {
155    pub schemas: Vec<String>,
156    pub id: Uuid,
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub external_id: Option<String>,
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub meta: Option<ScimMeta>,
161}
162
163#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema)]
164#[serde(rename_all = "camelCase")]
165pub struct ScimEntry {
166    pub schemas: Vec<String>,
167    pub id: Uuid,
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub external_id: Option<String>,
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub meta: Option<ScimMeta>,
172    #[serde(flatten)]
173    pub attrs: BTreeMap<String, ScimValue>,
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use crate::constants::RFC7643_USER;
180
181    #[test]
182    fn parse_scim_entry() {
183        let _ = tracing_subscriber::fmt::try_init();
184
185        let u: ScimEntry =
186            serde_json::from_str(RFC7643_USER).expect("Failed to parse RFC7643_USER");
187
188        tracing::trace!(?u);
189
190        let s = serde_json::to_string_pretty(&u).expect("Failed to serialise RFC7643_USER");
191        eprintln!("{}", s);
192    }
193
194    // =========================================================
195    // asymmetric serde tests
196
197    use serde::de::{self, Deserialize, Deserializer, Visitor};
198    use std::fmt;
199    use uuid::Uuid;
200
201    // -> For values, we need to be able to capture and handle "what if it's X" type? But
202    // we can't know the "intent" until we hit schema, so we have to preserve the string
203    // types as well. In this type, we make this *asymmetric*. When we parse we use
204    // this type which has the "maybes" but when we serialise, we use concrete types
205    // instead.
206
207    #[derive(Debug)]
208    #[allow(dead_code)]
209    enum TestB {
210        Integer(i64),
211        Decimal(f64),
212        MaybeUuid(Uuid, String),
213        String(String),
214    }
215
216    struct TestBVisitor;
217
218    impl Visitor<'_> for TestBVisitor {
219        type Value = TestB;
220
221        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
222            formatter.write_str("cheese")
223        }
224
225        fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
226        where
227            E: de::Error,
228        {
229            Ok(TestB::Decimal(v))
230        }
231
232        fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
233        where
234            E: de::Error,
235        {
236            Ok(TestB::Integer(v as i64))
237        }
238
239        fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
240        where
241            E: de::Error,
242        {
243            Ok(TestB::Integer(v))
244        }
245
246        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
247        where
248            E: de::Error,
249        {
250            Ok(if let Ok(u) = Uuid::parse_str(v) {
251                TestB::MaybeUuid(u, v.to_string())
252            } else {
253                TestB::String(v.to_string())
254            })
255        }
256    }
257
258    impl<'de> Deserialize<'de> for TestB {
259        fn deserialize<D>(deserializer: D) -> Result<TestB, D::Error>
260        where
261            D: Deserializer<'de>,
262        {
263            deserializer.deserialize_any(TestBVisitor)
264        }
265    }
266
267    #[test]
268    fn parse_enum_b() {
269        let x: TestB = serde_json::from_str("10").unwrap();
270        eprintln!("{:?}", x);
271
272        let x: TestB = serde_json::from_str("10.5").unwrap();
273        eprintln!("{:?}", x);
274
275        let x: TestB = serde_json::from_str(r#""550e8400-e29b-41d4-a716-446655440000""#).unwrap();
276        eprintln!("{:?}", x);
277
278        let x: TestB = serde_json::from_str(r#""Value""#).unwrap();
279        eprintln!("{:?}", x);
280    }
281
282    // In reverse when we serialise, we can simply use untagged on an enum.
283    // Potentially this lets us have more "scim" types for dedicated serialisations
284    // over the generic ones.
285
286    #[derive(Serialize, Debug, Deserialize, Clone)]
287    #[serde(rename_all = "lowercase", from = "&str", into = "String")]
288    enum TestC {
289        A,
290        B,
291        Unknown(String),
292    }
293
294    impl From<TestC> for String {
295        fn from(v: TestC) -> String {
296            match v {
297                TestC::A => "A".to_string(),
298                TestC::B => "B".to_string(),
299                TestC::Unknown(v) => v,
300            }
301        }
302    }
303
304    impl From<&str> for TestC {
305        fn from(v: &str) -> TestC {
306            match v {
307                "A" => TestC::A,
308                "B" => TestC::B,
309                _ => TestC::Unknown(v.to_string()),
310            }
311        }
312    }
313
314    #[test]
315    fn parse_enum_c() {
316        let x = serde_json::to_string(&TestC::A).unwrap();
317        eprintln!("{:?}", x);
318
319        let x = serde_json::to_string(&TestC::B).unwrap();
320        eprintln!("{:?}", x);
321
322        let x = serde_json::to_string(&TestC::Unknown("X".to_string())).unwrap();
323        eprintln!("{:?}", x);
324    }
325}