arcly_http_identity/federation/
scim.rs1use std::sync::Arc;
11
12use serde::{Deserialize, Serialize};
13
14use crate::error::{IdentityError, Result};
15use crate::identity::new_id;
16use crate::model::{AccountStatus, Identity};
17use crate::store::UserStore;
18
19const SCHEMA_USER: &str = "urn:ietf:params:scim:schemas:core:2.0:User";
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ScimName {
23 #[serde(default, rename = "givenName", skip_serializing_if = "Option::is_none")]
24 pub given_name: Option<String>,
25 #[serde(
26 default,
27 rename = "familyName",
28 skip_serializing_if = "Option::is_none"
29 )]
30 pub family_name: Option<String>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ScimEmail {
35 pub value: String,
36 #[serde(default)]
37 pub primary: bool,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct ScimUser {
43 pub schemas: Vec<String>,
44 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub id: Option<String>,
46 #[serde(rename = "userName")]
47 pub user_name: String,
48 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub name: Option<ScimName>,
50 #[serde(default, skip_serializing_if = "Vec::is_empty")]
51 pub emails: Vec<ScimEmail>,
52 #[serde(default = "default_true")]
54 pub active: bool,
55 #[serde(
56 default,
57 rename = "externalId",
58 skip_serializing_if = "Option::is_none"
59 )]
60 pub external_id: Option<String>,
61}
62
63fn default_true() -> bool {
64 true
65}
66
67impl ScimUser {
68 fn primary_email(&self) -> Option<&str> {
69 self.emails
70 .iter()
71 .find(|e| e.primary)
72 .or_else(|| self.emails.first())
73 .map(|e| e.value.as_str())
74 }
75}
76
77pub struct ScimProvisioner {
79 users: Arc<dyn UserStore>,
80 tenant: Option<String>,
81}
82
83impl ScimProvisioner {
84 pub fn new(users: Arc<dyn UserStore>, tenant: Option<String>) -> Self {
85 Self { users, tenant }
86 }
87
88 pub async fn create(&self, scim: &ScimUser) -> Result<ScimUser> {
90 let email = scim.primary_email().unwrap_or(&scim.user_name).to_owned();
91 if self
92 .users
93 .find_by_email(self.tenant.as_deref(), &email)
94 .await?
95 .is_some()
96 {
97 return Err(IdentityError::AlreadyExists);
98 }
99 let identity = Identity {
100 id: new_id(),
101 tenant: self.tenant.clone(),
102 email: Some(email),
103 email_verified: true,
105 phone: None,
106 phone_verified: false,
107 status: if scim.active {
108 AccountStatus::Active
109 } else {
110 AccountStatus::Suspended
111 },
112 roles: vec!["user".to_owned()],
113 perms: Vec::new(),
114 mfa: Default::default(),
115 attributes: Default::default(),
116 };
117 self.users.insert(&identity).await?;
118 Ok(to_scim(&identity))
119 }
120
121 pub async fn replace(&self, id: &str, scim: &ScimUser) -> Result<ScimUser> {
123 let mut identity = self
124 .users
125 .find_by_id(id)
126 .await?
127 .ok_or(IdentityError::NotFound)?;
128 if let Some(email) = scim.primary_email() {
129 identity.email = Some(email.to_owned());
130 }
131 identity.status = if scim.active {
132 AccountStatus::Active
133 } else {
134 AccountStatus::Suspended
135 };
136 self.users.update(&identity).await?;
137 Ok(to_scim(&identity))
138 }
139
140 pub async fn set_active(&self, id: &str, active: bool) -> Result<ScimUser> {
144 let mut identity = self
145 .users
146 .find_by_id(id)
147 .await?
148 .ok_or(IdentityError::NotFound)?;
149 identity.status = if active {
150 AccountStatus::Active
151 } else {
152 AccountStatus::Suspended
153 };
154 self.users.update(&identity).await?;
155 Ok(to_scim(&identity))
156 }
157
158 pub async fn get(&self, id: &str) -> Result<ScimUser> {
160 self.users
161 .find_by_id(id)
162 .await?
163 .map(|i| to_scim(&i))
164 .ok_or(IdentityError::NotFound)
165 }
166
167 pub async fn delete(&self, id: &str) -> Result<()> {
170 let mut identity = self
171 .users
172 .find_by_id(id)
173 .await?
174 .ok_or(IdentityError::NotFound)?;
175 identity.status = AccountStatus::Deleted;
176 self.users.update(&identity).await
177 }
178}
179
180fn to_scim(identity: &Identity) -> ScimUser {
181 ScimUser {
182 schemas: vec![SCHEMA_USER.to_owned()],
183 id: Some(identity.id.clone()),
184 user_name: identity
185 .email
186 .clone()
187 .unwrap_or_else(|| identity.id.clone()),
188 name: None,
189 emails: identity
190 .email
191 .as_ref()
192 .map(|e| {
193 vec![ScimEmail {
194 value: e.clone(),
195 primary: true,
196 }]
197 })
198 .unwrap_or_default(),
199 active: matches!(identity.status, AccountStatus::Active),
200 external_id: None,
201 }
202}