1use serde_json::{json, Value};
5use std::sync::LazyLock;
6
7pub const USER: &str = "urn:ietf:params:scim:schemas:core:2.0:User";
8pub const GROUP: &str = "urn:ietf:params:scim:schemas:core:2.0:Group";
9pub const ENTERPRISE: &str = "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User";
10pub const LIST_RESPONSE: &str = "urn:ietf:params:scim:api:messages:2.0:ListResponse";
11pub const SEARCH_REQUEST: &str = "urn:ietf:params:scim:api:messages:2.0:SearchRequest";
12pub const PATCH_OP: &str = "urn:ietf:params:scim:api:messages:2.0:PatchOp";
13pub const ERROR: &str = "urn:ietf:params:scim:api:messages:2.0:Error";
14
15pub static SCHEMAS: LazyLock<Vec<Value>> = LazyLock::new(|| {
16 serde_json::from_str(include_str!("rfc7643_schemas.json")).expect("embedded RFC 7643 schemas")
17});
18
19pub struct ResourceType {
21 pub name: &'static str,
22 pub endpoint: &'static str,
23 pub schema: &'static str,
24 pub extensions: &'static [&'static str],
25}
26
27pub const RESOURCE_TYPES: &[ResourceType] = &[
28 ResourceType {
29 name: "User",
30 endpoint: "/Users",
31 schema: USER,
32 extensions: &[ENTERPRISE],
33 },
34 ResourceType {
35 name: "Group",
36 endpoint: "/Groups",
37 schema: GROUP,
38 extensions: &[],
39 },
40];
41
42pub fn by_endpoint(segment: &str) -> Option<&'static ResourceType> {
43 RESOURCE_TYPES.iter().find(|r| &r.endpoint[1..] == segment)
44}
45
46pub fn by_name(name: &str) -> Option<&'static ResourceType> {
47 RESOURCE_TYPES
48 .iter()
49 .find(|r| r.name.eq_ignore_ascii_case(name))
50}
51
52pub fn schema(urn: &str) -> Option<&'static Value> {
53 SCHEMAS.iter().find(|s| {
54 s["id"]
55 .as_str()
56 .is_some_and(|id| id.eq_ignore_ascii_case(urn))
57 })
58}
59
60pub fn member<'a>(obj: &'a Value, name: &str) -> Option<&'a Value> {
62 obj.as_object()?
63 .iter()
64 .find(|(k, _)| k.eq_ignore_ascii_case(name))
65 .map(|(_, v)| v)
66}
67
68pub fn member_key(obj: &Value, name: &str) -> Option<String> {
69 obj.as_object()?
70 .keys()
71 .find(|k| k.eq_ignore_ascii_case(name))
72 .cloned()
73}
74
75fn find_attr<'a>(attrs: &'a Value, name: &str) -> Option<&'a Value> {
76 attrs.as_array()?.iter().find(|a| {
77 a["name"]
78 .as_str()
79 .is_some_and(|n| n.eq_ignore_ascii_case(name))
80 })
81}
82
83static COMMON: LazyLock<Value> = LazyLock::new(|| {
84 json!([
85 {"name": "id", "type": "string", "caseExact": true, "returned": "always", "mutability": "readOnly", "multiValued": false},
86 {"name": "externalId", "type": "string", "caseExact": true, "returned": "default", "mutability": "readWrite", "multiValued": false},
87 {"name": "schemas", "type": "reference", "caseExact": true, "returned": "always", "multiValued": true},
88 {"name": "meta", "type": "complex", "returned": "default", "mutability": "readOnly", "multiValued": false, "subAttributes": [
89 {"name": "resourceType", "type": "string", "caseExact": true},
90 {"name": "created", "type": "dateTime"},
91 {"name": "lastModified", "type": "dateTime"},
92 {"name": "location", "type": "reference", "caseExact": true},
93 {"name": "version", "type": "string", "caseExact": true}
94 ]}
95 ])
96});
97
98pub fn attribute(
101 rt: &ResourceType,
102 urn: Option<&str>,
103 attr: &str,
104 sub: Option<&str>,
105) -> Option<&'static Value> {
106 let base = match urn {
107 Some(u) if !u.eq_ignore_ascii_case(rt.schema) => {
108 if !rt.extensions.iter().any(|e| e.eq_ignore_ascii_case(u)) {
109 return None;
110 }
111 find_attr(&schema(u)?["attributes"], attr)?
112 }
113 _ => find_attr(&COMMON, attr)
114 .or_else(|| find_attr(&schema(rt.schema)?["attributes"], attr))?,
115 };
116 match sub {
117 None => Some(base),
118 Some(s) => find_attr(&base["subAttributes"], s),
119 }
120}
121
122pub fn case_exact(def: Option<&Value>) -> bool {
123 def.and_then(|d| d["caseExact"].as_bool()).unwrap_or(false)
124}
125
126pub fn returned(def: Option<&Value>) -> &'static str {
127 match def.and_then(|d| d["returned"].as_str()) {
128 Some("always") => "always",
129 Some("never") => "never",
130 Some("request") => "request",
131 _ => "default",
132 }
133}
134
135pub fn names_returned(rt: &ResourceType, wanted: &str) -> Vec<(Option<&'static str>, String)> {
137 let mut out = Vec::new();
138 for a in COMMON.as_array().into_iter().flatten() {
139 if a["returned"] == wanted {
140 out.push((None, a["name"].as_str().unwrap_or_default().to_owned()));
141 }
142 }
143 let mut add = |urn: Option<&'static str>, s: &str| {
144 for a in schema(s)
145 .and_then(|s| s["attributes"].as_array())
146 .into_iter()
147 .flatten()
148 {
149 if a["returned"] == wanted {
150 out.push((urn, a["name"].as_str().unwrap_or_default().to_owned()));
151 }
152 }
153 };
154 add(None, rt.schema);
155 for e in rt.extensions {
156 add(Some(e), e);
157 }
158 out
159}
160
161pub fn published(s: &Value, base: &str) -> Value {
163 let mut s = s.clone();
164 let id = s["id"].as_str().unwrap_or_default().to_owned();
165 s["schemas"] = json!(["urn:ietf:params:scim:schemas:core:2.0:Schema"]);
166 s["meta"] = json!({"resourceType": "Schema", "location": format!("{base}/Schemas/{id}")});
167 s
168}
169
170pub fn resource_type_resource(rt: &ResourceType, base: &str) -> Value {
171 json!({
172 "schemas": ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"],
173 "id": rt.name,
174 "name": rt.name,
175 "endpoint": rt.endpoint,
176 "description": format!("{} accounts", rt.name),
177 "schema": rt.schema,
178 "schemaExtensions": rt.extensions.iter().map(|e| json!({"schema": e, "required": false})).collect::<Vec<_>>(),
179 "meta": {"resourceType": "ResourceType", "location": format!("{base}/ResourceTypes/{}", rt.name)}
180 })
181}