1use crate::{AuthError, AuthResult};
3use async_trait::async_trait;
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6#[derive(Clone, Debug, Serialize, Deserialize)]
7#[serde(rename_all = "camelCase")]
8pub struct ScimManagedConnection {
9 pub id: String,
10 pub creation_request_id: String,
11 pub connection_id: String,
12 pub provisioning_domain_id: String,
13 pub status: String,
14 pub revision: i64,
15 pub created_at: DateTime<Utc>,
16 pub created_by: String,
17 pub decommission_started_at: Option<DateTime<Utc>>,
18 pub decommission_started_by: Option<String>,
19 pub decommissioned_at: Option<DateTime<Utc>>,
20 pub decommissioned_by: Option<String>,
21}
22#[derive(Clone, Debug, Serialize, Deserialize)]
23#[serde(rename_all = "camelCase")]
24pub struct ScimManagedCredential {
25 pub id: String,
26 pub connection_record_id: String,
27 pub credential_id: String,
28 pub token_digest: String,
29 pub hash_version: String,
30 pub active_slot_key: String,
31 pub status: String,
32 pub serialized_scopes: String,
33 pub expires_at: DateTime<Utc>,
34 pub created_at: DateTime<Utc>,
35 pub created_by: String,
36 pub last_used_at: Option<DateTime<Utc>>,
37 pub revoked_at: Option<DateTime<Utc>>,
38 pub revoked_by: Option<String>,
39 pub decommissioned_at: Option<DateTime<Utc>>,
40}
41pub struct CreateScimManagedConnection {
42 pub creation_request_id: String,
43 pub connection_id: String,
44 pub provisioning_domain_id: String,
45 pub actor_id: String,
46 pub credential_id: String,
47 pub token_digest: String,
48 pub serialized_scopes: String,
49 pub expires_at: DateTime<Utc>,
50 pub created_at: DateTime<Utc>,
51}
52#[derive(Clone)]
53pub struct RotateScimManagedCredential {
54 pub connection_id: String,
55 pub provisioning_domain_id: String,
56 pub actor_id: String,
57 pub credential_id: String,
58 pub token_digest: String,
59 pub serialized_scopes: String,
60 pub expires_at: DateTime<Utc>,
61 pub created_at: DateTime<Utc>,
62 pub max_active_credentials: usize,
63}
64#[derive(Clone)]
65pub struct RevokeScimManagedCredential {
66 pub connection_id: String,
67 pub provisioning_domain_id: String,
68 pub credential_id: String,
69 pub actor_id: String,
70}
71#[derive(Clone, Debug, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase")]
73pub struct ScimManagedConnectionEvent {
74 pub sequence: i64,
75 #[serde(rename = "type")]
76 pub kind: String,
77 pub actor_id: String,
78 pub credential_id: Option<String>,
79 pub created_at: DateTime<Utc>,
80}
81#[derive(Clone, Debug, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct ScimConnectionBinding {
84 pub id: String,
85 pub connection_id: String,
86 pub connection_key: String,
87 pub provisioning_domain_id: String,
88 pub created_at: DateTime<Utc>,
89 pub decommission_status: String,
90 pub decommission_cursor_user_id: Option<String>,
91 pub decommission_reconciled_user_count: i64,
92 pub decommission_batch_count: i64,
93 pub decommission_revision: i64,
94 pub decommissioned_at: Option<DateTime<Utc>>,
95 pub decommission_completed_at: Option<DateTime<Utc>>,
96 pub decommission_lease_id: Option<String>,
97 pub decommission_lease_expires_at: Option<DateTime<Utc>>,
98}
99#[derive(Clone)]
100pub struct DecommissionScimManagedConnection {
101 pub connection_id: String,
102 pub provisioning_domain_id: String,
103 pub actor_id: String,
104}
105pub struct ScimManagedConnectionState {
106 pub connection: ScimManagedConnection,
107 pub credentials: Vec<ScimManagedCredential>,
108}
109pub struct ScimManagedCreation {
110 pub connection: ScimManagedConnection,
111 pub credential: ScimManagedCredential,
112}
113#[derive(Clone)]
115pub struct ScimUserRecord {
116 pub id: String,
117 pub connection_id: String,
118 pub provisioning_domain_id: String,
119 pub user_name: String,
120 pub primary_email: String,
121 pub display_name: String,
122 pub active: bool,
123 pub external_id: Option<String>,
124 pub formatted_name: String,
125 pub given_name: Option<String>,
126 pub family_name: Option<String>,
127 pub serialized_emails: String,
128 pub serialized_attributes: Option<String>,
129 pub created_at: DateTime<Utc>,
130 pub updated_at: DateTime<Utc>,
131}
132#[derive(Clone)]
134pub struct ScimUserAttributes {
135 pub user_name: String,
136 pub user_name_key: String,
137 pub primary_email: String,
138 pub work_email_value_index: String,
139 pub email_value_index: String,
140 pub display_name: String,
141 pub formatted_name: String,
142 pub given_name: Option<String>,
143 pub family_name: Option<String>,
144 pub serialized_emails: String,
145 pub serialized_attributes: Option<String>,
146 pub external_id: Option<String>,
147 pub external_id_key: Option<String>,
148 pub active: bool,
149}
150pub struct ReplaceScimUser {
151 pub connection_id: String,
152 pub provisioning_domain_id: String,
153 pub attributes: ScimUserAttributes,
154 pub updated_at: DateTime<Utc>,
155}
156pub struct ScimUserMutationContext {
157 pub user_id: String,
158 pub source: ScimUserRecord,
159 pub manage_profile: bool,
160}
161pub struct CreateScimUser {
162 pub attributes: ScimUserAttributes,
163 pub connection_id: String,
164 pub provisioning_domain_id: String,
165 pub user_id: String,
166 pub connection_user_key: String,
167 pub order_key: String,
168 pub created_at: DateTime<Utc>,
169 pub manage_profile: bool,
170 pub tombstone_id: Option<String>,
171}
172#[derive(Clone)]
173pub struct ScimIdentityTombstone {
174 pub id: String,
175 pub provisioning_domain_id: String,
176 pub user_id: String,
177 pub profile: String,
178}
179pub struct CreateScimGroup {
181 pub connection_id: String,
182 pub provisioning_domain_id: String,
183 pub display_name: String,
184 pub display_name_key: String,
185 pub external_id: Option<String>,
186 pub external_id_key: Option<String>,
187 pub order_key: String,
188 pub member_ids: Vec<String>,
189 pub created_at: DateTime<Utc>,
190}
191#[derive(Clone)]
193pub struct ReplaceScimGroup {
194 pub connection_id: String,
195 pub provisioning_domain_id: String,
196 pub display_name: String,
197 pub display_name_key: String,
198 pub external_id: Option<String>,
199 pub external_id_key: Option<String>,
200 pub member_ids: Vec<String>,
201}
202pub struct ScimGroupRecord {
204 pub id: String,
205 pub connection_id: String,
206 pub provisioning_domain_id: String,
207 pub display_name: String,
208 pub external_id: Option<String>,
209 pub created_at: DateTime<Utc>,
210 pub updated_at: DateTime<Utc>,
211}
212pub struct ScimGroupMemberRecord {
213 pub id: String,
214 pub display_name: String,
215}
216pub struct ScimUserPage {
217 pub total_results: i64,
218 pub users: Vec<ScimUserRecord>,
219}
220pub struct ScimGroupPage {
221 pub total_results: i64,
222 pub groups: Vec<ScimGroupRecord>,
223}
224pub struct ScimGroupMemberships {
225 pub persisted_count: usize,
226 pub members: Vec<ScimGroupMemberRecord>,
227}
228#[async_trait]
229pub trait ScimManagedStore: Send + Sync {
230 async fn find_scim_identity_tombstone(
231 &self,
232 _connection: &str,
233 _key: &str,
234 ) -> AuthResult<Option<ScimIdentityTombstone>> {
235 Err(AuthError::not_implemented(
236 "SCIM identity tombstones are unsupported",
237 ))
238 }
239
240 async fn scim_user_domain_stable(&self, _connection: &str, _domain: &str) -> AuthResult<bool> {
241 Err(AuthError::config(
242 "SCIM resource persistence is not supported by this store",
243 ))
244 }
245 async fn list_scim_users(
246 &self,
247 _connection: &str,
248 _filters: &[(String, String)],
249 _offset: i64,
250 _count: i64,
251 ) -> AuthResult<ScimUserPage> {
252 Err(AuthError::config(
253 "SCIM resource persistence is not supported by this store",
254 ))
255 }
256
257 async fn scim_group_domain_stable(&self, _connection: &str, _domain: &str) -> AuthResult<bool> {
258 Err(AuthError::config(
259 "SCIM resource persistence is not supported by this store",
260 ))
261 }
262 async fn list_scim_groups(
263 &self,
264 _connection: &str,
265 _filters: &[(String, String)],
266 _offset: i64,
267 _count: i64,
268 ) -> AuthResult<ScimGroupPage> {
269 Err(AuthError::config(
270 "SCIM resource persistence is not supported by this store",
271 ))
272 }
273
274 async fn get_scim_user(
275 &self,
276 _id: &str,
277 _connection: &str,
278 ) -> AuthResult<Option<ScimUserRecord>> {
279 Err(AuthError::config(
280 "SCIM resource persistence is not supported by this store",
281 ))
282 }
283
284 async fn find_scim_managed_credential(
285 &self,
286 _credential_id: &str,
287 ) -> AuthResult<Option<ScimManagedCredential>> {
288 Err(AuthError::config(
289 "Managed SCIM persistence is not supported by this store",
290 ))
291 }
292 async fn find_scim_active_managed_connection(
293 &self,
294 _id: &str,
295 ) -> AuthResult<Option<ScimManagedConnection>> {
296 Err(AuthError::config(
297 "Managed SCIM persistence is not supported by this store",
298 ))
299 }
300 async fn record_scim_credential_use(
301 &self,
302 _id: &str,
303 _previous: Option<DateTime<Utc>>,
304 _at: DateTime<Utc>,
305 ) -> AuthResult<()> {
306 Err(AuthError::config(
307 "Managed SCIM persistence is not supported by this store",
308 ))
309 }
310
311 async fn active_scim_connection_binding(
312 &self,
313 _id: &str,
314 _domain: &str,
315 _key: &str,
316 ) -> AuthResult<ScimConnectionBinding> {
317 Err(AuthError::config(
318 "SCIM binding persistence is not supported by this store",
319 ))
320 }
321 async fn get_scim_group(
322 &self,
323 _id: &str,
324 _connection: &str,
325 ) -> AuthResult<Option<ScimGroupRecord>> {
326 Err(AuthError::config(
327 "SCIM resource persistence is not supported by this store",
328 ))
329 }
330 async fn scim_group_members(
331 &self,
332 _id: &str,
333 _connection: &str,
334 ) -> AuthResult<ScimGroupMemberships> {
335 Err(AuthError::config(
336 "SCIM resource persistence is not supported by this store",
337 ))
338 }
339
340 async fn acquire_scim_decommission_lease(
341 &self,
342 _binding: ScimConnectionBinding,
343 ) -> AuthResult<Option<ScimConnectionBinding>> {
344 Err(AuthError::config(
345 "SCIM binding persistence is not supported by this store",
346 ))
347 }
348 async fn release_scim_decommission_lease(&self, _id: &str, _lease: &str) -> AuthResult<()> {
349 Err(AuthError::config(
350 "SCIM binding persistence is not supported by this store",
351 ))
352 }
353
354 async fn scim_projection_candidates(
355 &self,
356 _domain: &str,
357 _cursor: Option<&str>,
358 ) -> AuthResult<Vec<ScimProjectionCandidate>> {
359 Err(AuthError::config(
360 "SCIM projection persistence is not supported by this store",
361 ))
362 }
363
364 async fn decommission_scim_managed_connection(
365 &self,
366 _data: DecommissionScimManagedConnection,
367 _complete: bool,
368 ) -> AuthResult<Option<ScimManagedConnection>> {
369 Err(AuthError::config(
370 "Managed SCIM persistence is not supported by this store",
371 ))
372 }
373
374 async fn terminal_scim_connection_binding(
375 &self,
376 _id: &str,
377 _domain: Option<&str>,
378 _key: &str,
379 ) -> AuthResult<ScimConnectionBinding> {
380 Err(AuthError::config(
381 "SCIM binding persistence is not supported by this store",
382 ))
383 }
384
385 async fn list_scim_managed_connection_events(
386 &self,
387 _id: &str,
388 _domain: &str,
389 ) -> AuthResult<Option<Vec<ScimManagedConnectionEvent>>> {
390 Err(AuthError::config(
391 "Managed SCIM persistence is not supported by this store",
392 ))
393 }
394
395 async fn revoke_scim_managed_credential(
396 &self,
397 _data: RevokeScimManagedCredential,
398 ) -> AuthResult<Option<ScimManagedConnection>> {
399 Err(AuthError::config(
400 "Managed SCIM persistence is not supported by this store",
401 ))
402 }
403
404 async fn rotate_scim_managed_credential(
405 &self,
406 _data: RotateScimManagedCredential,
407 ) -> AuthResult<Option<ScimManagedCreation>> {
408 Err(AuthError::config(
409 "Managed SCIM persistence is not supported by this store",
410 ))
411 }
412
413 async fn get_scim_managed_connection(
414 &self,
415 _id: &str,
416 _domain: &str,
417 ) -> AuthResult<Option<ScimManagedConnectionState>> {
418 Err(AuthError::config(
419 "Managed SCIM persistence is not supported by this store",
420 ))
421 }
422
423 async fn list_scim_managed_connections(
424 &self,
425 _domain: &str,
426 ) -> AuthResult<Vec<ScimManagedConnection>> {
427 Err(AuthError::config(
428 "Managed SCIM persistence is not supported by this store",
429 ))
430 }
431
432 async fn create_scim_managed_connection(
436 &self,
437 _data: CreateScimManagedConnection,
438 ) -> AuthResult<ScimManagedCreation> {
439 Err(AuthError::config(
440 "Managed SCIM persistence is not supported by this store",
441 ))
442 }
443}
444pub fn scim_creation_conflict() -> AuthError {
445 AuthError::Api {
446 status: 409,
447 code: Some("SCIM_MANAGED_CREATION_REQUEST_ID_CONFLICT".into()),
448 message: "Managed SCIM connection creation request ID already exists".into(),
449 }
450}
451
452pub fn scim_managed_conflict(message: &str) -> AuthError {
453 AuthError::Api {
454 status: 409,
455 code: None,
456 message: message.into(),
457 }
458}
459
460pub fn validate_scim_binding_domain(
461 binding: &ScimConnectionBinding,
462 domain: Option<&str>,
463) -> AuthResult<()> {
464 if let Some(domain) = domain
465 && domain != binding.provisioning_domain_id
466 {
467 return Err(AuthError::internal(format!(
468 "SCIM connection \"{}\" is already bound to provisioning domain \"{}\".",
469 binding.connection_id, binding.provisioning_domain_id
470 )));
471 }
472 Ok(())
473}
474
475#[derive(Clone, Debug, Serialize, Deserialize)]
476#[serde(rename_all = "camelCase")]
477pub struct ScimIdentitySource {
478 pub id: String,
479 pub connection_id: String,
480 pub provisioning_domain_id: String,
481 pub active: bool,
482}
483pub fn sort_scim_sources(sources: &mut [ScimIdentitySource]) -> AuthResult<()> {
488 let collator = icu_collator::Collator::try_new(
489 icu_collator::CollatorPreferences::default(),
490 icu_collator::options::CollatorOptions::default(),
491 )
492 .map_err(|error| AuthError::internal(error.to_string()))?;
493 sources.sort_by(|left, right| collator.compare(&left.id, &right.id));
494 Ok(())
495}
496
497#[derive(Clone, Debug, Serialize, Deserialize)]
498#[serde(rename_all = "camelCase")]
499pub struct ScimProjectedUserState {
500 pub provisioning_domain_id: String,
501 pub user_id: String,
502 pub active: bool,
503 pub sources: Vec<ScimIdentitySource>,
504 pub grants: Vec<serde_json::Value>,
505}
506
507#[derive(Clone, Debug)]
508pub struct ScimProjectionCandidate {
509 pub id: String,
510 pub user_id: String,
511}
512pub struct ScimProjectionPreparedUser {
513 pub state: ScimProjectedUserState,
514 pub removed_grant_ids: Vec<String>,
515 pub existing_grants: Vec<(String, String)>,
516 pub role_sources: Vec<ScimRoleMappingInput>,
517}
518
519#[derive(Clone, Debug, Serialize, Deserialize)]
520#[serde(tag = "type", rename_all = "lowercase")]
521pub enum ScimAuthorizationSource {
522 Group {
523 id: String,
524 #[serde(rename = "externalId", skip_serializing_if = "Option::is_none")]
525 external_id: Option<String>,
526 #[serde(rename = "displayName")]
527 display_name: String,
528 },
529}
530#[derive(Clone, Debug, Serialize, Deserialize)]
531#[serde(rename_all = "camelCase")]
532pub struct ScimRoleMappingInput {
533 pub connection_id: String,
534 pub provisioning_domain_id: String,
535 pub scim_user_id: String,
536 pub user_id: String,
537 pub source: ScimAuthorizationSource,
538}
539#[derive(Clone, Debug, Serialize, Deserialize)]
540#[serde(rename_all = "camelCase")]
541pub struct ScimRoleExistenceInput {
542 pub connection_id: String,
543 pub provisioning_domain_id: String,
544 pub role: String,
545}
546
547#[derive(Clone)]
548pub struct CreateScimProjectionGrant {
549 pub connection_id: String,
550 pub provisioning_domain_id: String,
551 pub scim_user_id: String,
552 pub user_id: String,
553 pub source_id: String,
554 pub source_value: String,
555 pub role: String,
556 pub grant_key: String,
557 pub created_at: DateTime<Utc>,
558}
559
560#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
561#[serde(rename_all = "camelCase")]
562pub struct ScimIdentityState {
563 pub user_id: String,
564 pub active: bool,
565 #[serde(skip_serializing_if = "Option::is_none")]
566 pub profile_source_id: Option<String>,
567 pub sources: Vec<ScimIdentitySource>,
568}
569
570pub fn scim_group_membership_key(connection: &str, group: &str, user: &str) -> AuthResult<String> {
574 use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
575 use sha2::{Digest, Sha256};
576 Ok(URL_SAFE_NO_PAD.encode(Sha256::digest(serde_json::to_vec(&[
577 "scim-group-member",
578 connection,
579 group,
580 user,
581 ])?)))
582}