1use alibi_core::endpoint::{EndpointCall, EndpointDefinition, EndpointResponse, ServerEndpoint};
3use alibi_core::{
4 AuthContext, AuthError, AuthPlugin, AuthRequest, AuthResponse, AuthResult, AuthRoute,
5 AuthSchema, HttpMethod,
6};
7use async_trait::async_trait;
8use serde_json::{Value, json};
9mod authentication;
10mod collection;
11mod compatibility;
12mod managed;
13mod projection;
14mod resources;
15mod user_attributes;
16mod user_input;
17mod user_patch;
18mod user_profile;
19pub use authentication::{ScimBearerTokenVerificationInput, ScimBearerTokenVerifier};
20pub use managed::{ScimManagedConnectionInput, ScimManagedCredentialRotationInput};
21pub use projection::{
22 ConfiguredScimPlugin, ScimAuthorizationSource, ScimIdentity, ScimIdentityResolution,
23 ScimIdentityResolutionInput, ScimIdentitySource, ScimIdentityState, ScimProjectedUserState,
24 ScimProjection, ScimRoleExistenceInput, ScimRoleMappingInput, ScimRoleProjection,
25};
26
27#[derive(Clone)]
29pub struct ScimCredential {
30 pub id: String,
31 pub token: String,
32 pub scopes: Option<Vec<String>>,
33 pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
34}
35impl std::fmt::Debug for ScimCredential {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 f.debug_struct("ScimCredential")
38 .field("id", &self.id)
39 .field("scopes", &self.scopes)
40 .field("expires_at", &self.expires_at)
41 .finish_non_exhaustive()
42 }
43}
44impl ScimCredential {
45 #[must_use]
46 pub fn new(id: impl Into<String>, token: impl Into<String>) -> Self {
47 Self {
48 id: id.into(),
49 token: token.into(),
50 scopes: None,
51 expires_at: None,
52 }
53 }
54}
55#[derive(Clone, Debug)]
57pub struct ScimConnectionOptions {
58 pub id: String,
59 pub provisioning_domain_id: Option<String>,
60 pub credentials: Vec<ScimCredential>,
61}
62#[derive(Clone)]
64pub struct ScimManagedConnectionOptions {
65 pub credential_hash_secret: String,
66 pub max_active_credentials: usize,
67 pub last_used_write_interval_seconds: u64,
68}
69impl std::fmt::Debug for ScimManagedConnectionOptions {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 f.debug_struct("ScimManagedConnectionOptions")
72 .field("max_active_credentials", &self.max_active_credentials)
73 .field(
74 "last_used_write_interval_seconds",
75 &self.last_used_write_interval_seconds,
76 )
77 .finish_non_exhaustive()
78 }
79}
80impl ScimManagedConnectionOptions {
81 #[must_use]
82 pub fn new(secret: impl Into<String>) -> Self {
83 Self {
84 credential_hash_secret: secret.into(),
85 max_active_credentials: 5,
86 last_used_write_interval_seconds: 300,
87 }
88 }
89}
90#[derive(Clone, Debug, Default)]
92pub struct ScimCompatibilityOptions {
93 pub microsoft_entra: Option<ScimMicrosoftEntraCompatibilityOptions>,
94}
95#[derive(Clone, Debug, Default)]
97pub struct ScimMicrosoftEntraCompatibilityOptions {
98 pub accept_legacy_group_schema: bool,
99}
100
101#[derive(Clone, Debug, Default)]
102pub struct ScimPlugin {
103 managed_connections: Option<ScimManagedConnectionOptions>,
104 connections: Vec<ScimConnectionOptions>,
105 has_token_verifier: bool,
106 accept_legacy_group_schema: bool,
107}
108impl ScimPlugin {
109 #[must_use]
110 pub const fn new() -> Self {
111 Self {
112 managed_connections: None,
113 connections: Vec::new(),
114 has_token_verifier: false,
115 accept_legacy_group_schema: false,
116 }
117 }
118 #[must_use]
120 pub fn compatibility(mut self, options: ScimCompatibilityOptions) -> Self {
121 self.accept_legacy_group_schema = options
122 .microsoft_entra
123 .is_some_and(|options| options.accept_legacy_group_schema);
124 self
125 }
126 #[must_use]
127 pub fn managed_connections(mut self, options: ScimManagedConnectionOptions) -> Self {
128 self.managed_connections = Some(options);
129 self
130 }
131 #[must_use]
132 pub fn connections(mut self, connections: Vec<ScimConnectionOptions>) -> Self {
133 self.connections = connections;
134 self
135 }
136 pub fn delete_user_endpoint(id: &str) -> AuthResult<ServerEndpoint<()>> {
140 ServerEndpoint::new("scim", "deleteSCIMUser").with_params(&json!({"userId":id}))
141 }
142 pub fn patch_group_endpoint(id: &str) -> AuthResult<ServerEndpoint<Value>> {
146 ServerEndpoint::new("scim", "patchSCIMGroup").with_params(&json!({"groupId":id}))
147 }
148 pub fn replace_group_endpoint(id: &str) -> AuthResult<ServerEndpoint<Value>> {
152 ServerEndpoint::new("scim", "replaceSCIMGroup").with_params(&json!({"groupId":id}))
153 }
154 pub fn delete_group_endpoint(id: &str) -> AuthResult<ServerEndpoint<()>> {
158 ServerEndpoint::new("scim", "deleteSCIMGroup").with_params(&json!({"groupId":id}))
159 }
160 pub fn patch_user_endpoint(id: &str) -> AuthResult<ServerEndpoint<Value>> {
164 ServerEndpoint::new("scim", "patchSCIMUser").with_params(&json!({"userId":id}))
165 }
166 pub fn replace_user_endpoint(id: &str) -> AuthResult<ServerEndpoint<Value>> {
170 ServerEndpoint::new("scim", "replaceSCIMUser").with_params(&json!({"userId":id}))
171 }
172 #[must_use]
174 pub const fn create_user_endpoint() -> ServerEndpoint<Value> {
175 ServerEndpoint::new("scim", "createSCIMUser")
176 }
177 #[must_use]
179 pub const fn create_group_endpoint() -> ServerEndpoint<Value> {
180 ServerEndpoint::new("scim", "createSCIMGroup")
181 }
182 #[must_use]
184 pub const fn users_endpoint() -> ServerEndpoint<Value> {
185 ServerEndpoint::new("scim", "listSCIMUsers")
186 }
187 #[must_use]
189 pub const fn groups_endpoint() -> ServerEndpoint<Value> {
190 ServerEndpoint::new("scim", "listSCIMGroups")
191 }
192 pub fn user_endpoint(id: &str) -> AuthResult<ServerEndpoint<Value>> {
196 ServerEndpoint::new("scim", "getSCIMUser").with_params(&json!({"userId":id}))
197 }
198 pub fn group_endpoint(id: &str) -> AuthResult<ServerEndpoint<Value>> {
202 ServerEndpoint::new("scim", "getSCIMGroup").with_params(&json!({"groupId":id}))
203 }
204 #[must_use]
206 pub const fn service_provider_config_endpoint() -> ServerEndpoint<Value> {
207 ServerEndpoint::new("scim", "getSCIMServiceProviderConfig")
208 }
209 #[must_use]
211 pub const fn resource_types_endpoint() -> ServerEndpoint<Value> {
212 ServerEndpoint::new("scim", "getSCIMResourceTypes")
213 }
214 #[must_use]
216 pub const fn schemas_endpoint() -> ServerEndpoint<Value> {
217 ServerEndpoint::new("scim", "getSCIMSchemas")
218 }
219 fn schemas<S: AuthSchema>(ctx: &AuthContext<S>) -> AuthResult<Value> {
220 let mut schemas: Vec<Value> = serde_json::from_str(include_str!("scim/schemas.json"))?;
223 let base = format!(
224 "{}{}",
225 ctx.config.base_url.trim_end_matches('/'),
226 ctx.config.base_path.trim_end_matches('/')
227 );
228 for schema in &mut schemas {
229 if let Some(meta) = schema.get_mut("meta").and_then(Value::as_object_mut)
230 && let Some(path) = meta.get("location").and_then(Value::as_str)
231 {
232 let location = format!("{base}{path}");
233 _ = meta.insert("location".into(), Value::String(location));
234 }
235 }
236 Ok(
237 json!({"schemas":["urn:ietf:params:scim:api:messages:2.0:ListResponse"],"totalResults":schemas.len(),"startIndex":1,"itemsPerPage":schemas.len(),"Resources":schemas}),
238 )
239 }
240 pub fn schema_endpoint(id: &str) -> AuthResult<ServerEndpoint<Value>> {
244 ServerEndpoint::new("scim", "getSCIMSchema").with_params(&json!({"schemaId":id}))
245 }
246 fn schema<S: AuthSchema>(ctx: &AuthContext<S>, id: &str) -> AuthResult<EndpointResponse> {
247 let decoded = urlencoding::decode(id).unwrap_or(std::borrow::Cow::Borrowed(id));
248 let schemas = Self::schemas(ctx)?;
249 let schema = schemas
250 .get("Resources")
251 .and_then(Value::as_array)
252 .and_then(|schemas| {
253 schemas.iter().find(|schema| {
254 schema.get("id").and_then(Value::as_str) == Some(decoded.as_ref())
255 })
256 });
257 Self::discovery_result(schema, "Schema not found")
258 }
259 fn discovery_result(resource: Option<&Value>, detail: &str) -> AuthResult<EndpointResponse> {
260 let response = if let Some(resource) = resource {
261 EndpointResponse::json(resource)?
262 } else {
263 EndpointResponse::error(AuthError::Api {status:404,code:None,message:detail.into()})
264 .with_error_body(alibi_core::utils::json::parse_value(&json!({"schemas":["urn:ietf:params:scim:api:messages:2.0:Error"],"status":"404","detail":detail}).to_string())?)
265 };
266 Ok(response.with_header("x-better-auth-scim-response", "1"))
267 }
268 pub fn resource_type_endpoint(id: &str) -> AuthResult<ServerEndpoint<Value>> {
272 ServerEndpoint::new("scim", "getSCIMResourceType")
273 .with_params(&json!({"resourceTypeId":id}))
274 }
275 fn resource_type<S: AuthSchema>(
276 ctx: &AuthContext<S>,
277 id: Option<&str>,
278 ) -> AuthResult<EndpointResponse> {
279 let types = Self::resource_types(ctx);
280 let resource = types
281 .get("Resources")
282 .and_then(Value::as_array)
283 .and_then(|types| {
284 types
285 .iter()
286 .find(|resource| resource.get("id").and_then(Value::as_str) == id)
287 });
288 Self::discovery_result(resource, "Resource type not found")
289 }
290 fn resource_types<S: AuthSchema>(ctx: &AuthContext<S>) -> Value {
291 let base = format!(
292 "{}{}",
293 ctx.config.base_url.trim_end_matches('/'),
294 ctx.config.base_path.trim_end_matches('/')
295 );
296 json!({"schemas":["urn:ietf:params:scim:api:messages:2.0:ListResponse"],"totalResults":2,"startIndex":1,"itemsPerPage":2,"Resources":[
297 {"schemas":["urn:ietf:params:scim:schemas:core:2.0:ResourceType"],"id":"User","name":"User","endpoint":"/Users","description":"User Account","schema":"urn:ietf:params:scim:schemas:core:2.0:User","schemaExtensions":[{"schema":"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User","required":false}],"meta":{"resourceType":"ResourceType","location":format!("{base}/scim/v2/ResourceTypes/User")}},
298 {"schemas":["urn:ietf:params:scim:schemas:core:2.0:ResourceType"],"id":"Group","name":"Group","endpoint":"/Groups","description":"Group","schema":"urn:ietf:params:scim:schemas:core:2.0:Group","meta":{"resourceType":"ResourceType","location":format!("{base}/scim/v2/ResourceTypes/Group")}}
299 ]})
300 }
301 fn service_provider_config<S: AuthSchema>(ctx: &AuthContext<S>) -> Value {
302 json!({
303 "schemas":["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
304 "patch":{"supported":true}, "bulk":{"supported":false,"maxOperations":0,"maxPayloadSize":0},
305 "filter":{"supported":true,"maxResults":100}, "changePassword":{"supported":false},
306 "sort":{"supported":false},"etag":{"supported":false},
307 "authenticationSchemes":[{"name":"OAuth Bearer Token","description":"Authentication using a bearer token in the Authorization header.","specUri":"https://www.rfc-editor.org/info/rfc6750","type":"oauthbearertoken","primary":true}],
308 "meta":{"resourceType":"ServiceProviderConfig","location":format!("{}{}{}",ctx.config.base_url.trim_end_matches('/'),ctx.config.base_path.trim_end_matches('/'),"/scim/v2/ServiceProviderConfig")}
309 })
310 }
311}
312#[async_trait]
313impl<S: AuthSchema> AuthPlugin<S> for ScimPlugin {
314 async fn on_http_request_action(
315 &self,
316 req: &AuthRequest,
317 _ctx: &AuthContext<S>,
318 ) -> AuthResult<Option<alibi_core::HttpRequestAction>> {
319 let path = req.url().map_or_else(|| req.path(), url::Url::path);
320 if !path.contains("/scim/v2/") {
321 return Ok(None);
322 }
323 if req.method() == &HttpMethod::Delete {
324 let mut replacement = req.clone();
325 replacement.body = None;
326 return Ok(Some(alibi_core::HttpRequestAction::ReplaceRequest(
327 Box::new(replacement),
328 )));
329 }
330 if !matches!(
331 req.method(),
332 HttpMethod::Post | HttpMethod::Put | HttpMethod::Patch
333 ) {
334 return Ok(None);
335 }
336 let media = req
337 .headers
338 .iter()
339 .find_map(|(name, value)| name.eq_ignore_ascii_case("content-type").then_some(value))
340 .and_then(|value| value.split(';').next())
341 .map(alibi_core::utils::javascript::trim);
342 if !media.is_some_and(|media| {
343 media.eq_ignore_ascii_case("application/json")
344 || media.eq_ignore_ascii_case("application/scim+json")
345 }) {
346 return Ok(Some(alibi_core::HttpRequestAction::Respond(
347 resources::to_http_response(&resources::error(
348 415,
349 "SCIM requests must use application/scim+json or application/json",
350 None,
351 ))?,
352 )));
353 }
354 let Ok(mut body) = alibi_core::utils::json::from_slice::<alibi_core::utils::json::JsValue>(
355 req.body.as_deref().unwrap_or_default(),
356 ) else {
357 return Ok(Some(alibi_core::HttpRequestAction::Respond(
358 resources::to_http_response(&resources::error(
359 400,
360 "SCIM request body must contain valid JSON",
361 Some("invalidSyntax"),
362 ))?,
363 )));
364 };
365 match compatibility::normalize_group(
366 path,
367 req.method(),
368 &mut body,
369 self.accept_legacy_group_schema,
370 ) {
371 Ok(false) => Ok(None),
372 Ok(true) => {
373 let mut replacement = req.clone();
374 replacement
375 .headers
376 .retain(|name, _| !name.eq_ignore_ascii_case("content-length"));
377 replacement.body = Some(alibi_core::utils::json::to_string(&body)?.into_bytes());
378 Ok(Some(alibi_core::HttpRequestAction::ReplaceRequest(
379 Box::new(replacement),
380 )))
381 }
382 Err(detail) => Ok(Some(alibi_core::HttpRequestAction::Respond(
383 resources::to_http_response(&resources::error(400, detail, Some("invalidValue")))?,
384 ))),
385 }
386 }
387
388 route_openapi_metadata!(S);
389 fn name(&self) -> &'static str {
390 "scim"
391 }
392 fn allowed_media_types(&self, route: &AuthRoute) -> Vec<&'static str> {
393 if (route.path.starts_with("/scim/v2/Users/")
395 && matches!(route.method, HttpMethod::Put | HttpMethod::Patch))
396 || route.path == "/scim/v2/Users"
397 || route.path == "/scim/v2/Groups"
398 || (route.path.starts_with("/scim/v2/Groups/")
399 && matches!(route.method, HttpMethod::Put | HttpMethod::Patch))
400 {
401 Vec::new()
402 } else {
403 vec!["application/json"]
404 }
405 }
406 fn routes(&self) -> Vec<AuthRoute> {
407 vec![
408 AuthRoute::new(
409 HttpMethod::Patch,
410 "/scim/v2/Users/{userId}",
411 "patchSCIMUser",
412 ),
413 AuthRoute::put("/scim/v2/Users/{userId}", "replaceSCIMUser"),
414 AuthRoute::post("/scim/v2/Users", "createSCIMUser"),
415 AuthRoute::post("/scim/v2/Groups", "createSCIMGroup"),
416 AuthRoute::delete("/scim/v2/Users/{userId}", "deleteSCIMUser"),
417 AuthRoute::put("/scim/v2/Groups/{groupId}", "replaceSCIMGroup"),
418 AuthRoute::new(
419 HttpMethod::Patch,
420 "/scim/v2/Groups/{groupId}",
421 "patchSCIMGroup",
422 ),
423 AuthRoute::delete("/scim/v2/Groups/{groupId}", "deleteSCIMGroup"),
424 AuthRoute::get("/scim/v2/Users", "listSCIMUsers"),
425 AuthRoute::get("/scim/v2/Groups", "listSCIMGroups"),
426 AuthRoute::get("/scim/v2/Users/{userId}", "getSCIMUser"),
427 AuthRoute::get("/scim/v2/Groups/{groupId}", "getSCIMGroup"),
428 AuthRoute::get("/scim/v2/Schemas", "getSCIMSchemas"),
429 AuthRoute::get("/scim/v2/Schemas/{schemaId}", "getSCIMSchema"),
430 AuthRoute::get("/scim/v2/ResourceTypes", "getSCIMResourceTypes"),
431 AuthRoute::get(
432 "/scim/v2/ResourceTypes/{resourceTypeId}",
433 "getSCIMResourceType",
434 ),
435 AuthRoute::get(
436 "/scim/v2/ServiceProviderConfig",
437 "getSCIMServiceProviderConfig",
438 ),
439 ]
440 }
441 fn server_endpoints(&self) -> Vec<EndpointDefinition> {
442 vec![
443 crate::endpoint::definition(
444 "deleteSCIMUser",
445 "deleteSCIMUser",
446 Some("/scim/v2/Users/:userId"),
447 HttpMethod::Delete,
448 ),
449 crate::endpoint::definition(
450 "patchSCIMGroup",
451 "patchSCIMGroup",
452 Some("/scim/v2/Groups/:groupId"),
453 HttpMethod::Patch,
454 ),
455 crate::endpoint::definition(
456 "replaceSCIMGroup",
457 "replaceSCIMGroup",
458 Some("/scim/v2/Groups/:groupId"),
459 HttpMethod::Put,
460 ),
461 crate::endpoint::definition(
462 "deleteSCIMGroup",
463 "deleteSCIMGroup",
464 Some("/scim/v2/Groups/:groupId"),
465 HttpMethod::Delete,
466 ),
467 crate::endpoint::definition(
468 "patchSCIMUser",
469 "patchSCIMUser",
470 Some("/scim/v2/Users/:userId"),
471 HttpMethod::Patch,
472 ),
473 crate::endpoint::definition(
474 "replaceSCIMUser",
475 "replaceSCIMUser",
476 Some("/scim/v2/Users/:userId"),
477 HttpMethod::Put,
478 ),
479 crate::endpoint::definition(
480 "createSCIMUser",
481 "createSCIMUser",
482 Some("/scim/v2/Users"),
483 HttpMethod::Post,
484 ),
485 crate::endpoint::definition(
486 "createSCIMGroup",
487 "createSCIMGroup",
488 Some("/scim/v2/Groups"),
489 HttpMethod::Post,
490 ),
491 crate::endpoint::definition(
492 "listSCIMUsers",
493 "listSCIMUsers",
494 Some("/scim/v2/Users"),
495 HttpMethod::Get,
496 ),
497 crate::endpoint::definition(
498 "listSCIMGroups",
499 "listSCIMGroups",
500 Some("/scim/v2/Groups"),
501 HttpMethod::Get,
502 ),
503 crate::endpoint::definition(
504 "getSCIMUser",
505 "getSCIMUser",
506 Some("/scim/v2/Users/:userId"),
507 HttpMethod::Get,
508 ),
509 crate::endpoint::definition(
510 "getSCIMGroup",
511 "getSCIMGroup",
512 Some("/scim/v2/Groups/:groupId"),
513 HttpMethod::Get,
514 ),
515 crate::endpoint::definition(
516 "reconcileSCIMProjection",
517 "reconcileSCIMProjection",
518 None,
519 HttpMethod::Post,
520 ),
521 crate::endpoint::definition(
522 "decommissionSCIMManagedConnection",
523 "decommissionSCIMManagedConnection",
524 None,
525 HttpMethod::Post,
526 ),
527 crate::endpoint::definition(
528 "decommissionSCIMConnection",
529 "decommissionSCIMConnection",
530 None,
531 HttpMethod::Post,
532 ),
533 crate::endpoint::definition(
534 "listSCIMManagedConnectionEvents",
535 "listSCIMManagedConnectionEvents",
536 None,
537 HttpMethod::Post,
538 ),
539 crate::endpoint::definition(
540 "revokeSCIMManagedCredential",
541 "revokeSCIMManagedCredential",
542 None,
543 HttpMethod::Post,
544 ),
545 crate::endpoint::definition(
546 "rotateSCIMManagedCredential",
547 "rotateSCIMManagedCredential",
548 None,
549 HttpMethod::Post,
550 ),
551 crate::endpoint::definition(
552 "getSCIMManagedConnection",
553 "getSCIMManagedConnection",
554 None,
555 HttpMethod::Post,
556 ),
557 crate::endpoint::definition(
558 "listSCIMManagedConnections",
559 "listSCIMManagedConnections",
560 None,
561 HttpMethod::Post,
562 ),
563 crate::endpoint::definition(
564 "createSCIMManagedConnection",
565 "createSCIMManagedConnection",
566 None,
567 HttpMethod::Post,
568 ),
569 crate::endpoint::definition(
570 "getSCIMSchema",
571 "getSCIMSchema",
572 Some("/scim/v2/Schemas/:schemaId"),
573 HttpMethod::Get,
574 ),
575 crate::endpoint::definition(
576 "getSCIMSchemas",
577 "getSCIMSchemas",
578 Some("/scim/v2/Schemas"),
579 HttpMethod::Get,
580 ),
581 crate::endpoint::definition(
582 "getSCIMResourceType",
583 "getSCIMResourceType",
584 Some("/scim/v2/ResourceTypes/:resourceTypeId"),
585 HttpMethod::Get,
586 ),
587 crate::endpoint::definition(
588 "getSCIMResourceTypes",
589 "getSCIMResourceTypes",
590 Some("/scim/v2/ResourceTypes"),
591 HttpMethod::Get,
592 ),
593 crate::endpoint::definition(
594 "getSCIMServiceProviderConfig",
595 "getSCIMServiceProviderConfig",
596 Some("/scim/v2/ServiceProviderConfig"),
597 HttpMethod::Get,
598 ),
599 ]
600 }
601 fn validate_endpoint(
602 &self,
603 call: &EndpointCall,
604 _ctx: &AuthContext<S>,
605 ) -> AuthResult<alibi_core::endpoint::EndpointInput> {
606 if matches!(
607 call.operation_id(),
608 "reconcileSCIMProjection"
609 | "decommissionSCIMManagedConnection"
610 | "decommissionSCIMConnection"
611 | "listSCIMManagedConnectionEvents"
612 | "revokeSCIMManagedCredential"
613 | "createSCIMManagedConnection"
614 | "listSCIMManagedConnections"
615 | "getSCIMManagedConnection"
616 | "rotateSCIMManagedCredential"
617 ) {
618 managed::validate(call)
619 } else {
620 Ok(alibi_core::endpoint::EndpointInput {
621 body: call.body().cloned(),
622 query: call.query().cloned(),
623 })
624 }
625 }
626 async fn on_init(&self, _ctx: &mut alibi_core::AuthInitContext<S>) -> AuthResult<()> {
627 self.validate_connections()?;
628 if let Some(options) = &self.managed_connections {
629 if options.credential_hash_secret.encode_utf16().count() < 32 {
630 return Err(AuthError::config(
631 "SCIM managed credentialHashSecret must contain at least 32 characters.",
632 ));
633 }
634 if !(1..=100).contains(&options.max_active_credentials) {
635 return Err(AuthError::config(
636 "SCIM managed maxActiveCredentials must be an integer between 1 and 100.",
637 ));
638 }
639 }
640 Ok(())
641 }
642 async fn on_endpoint(
643 &self,
644 call: &EndpointCall,
645 ctx: &AuthContext<S>,
646 ) -> AuthResult<EndpointResponse> {
647 if call.operation_id() == "deleteSCIMUser" {
648 return self.delete_user(call, ctx).await;
649 }
650 if call.operation_id() == "patchSCIMGroup" {
651 return self.patch_group(call, ctx).await;
652 }
653 if call.operation_id() == "replaceSCIMGroup" {
654 return self.replace_group(call, ctx).await;
655 }
656 if call.operation_id() == "deleteSCIMGroup" {
657 return self.delete_group(call, ctx).await;
658 }
659 if call.operation_id() == "patchSCIMUser" {
660 return self.patch_user(call, ctx).await;
661 }
662 if call.operation_id() == "replaceSCIMUser" {
663 return self.replace_user(call, ctx).await;
664 }
665 if call.operation_id() == "createSCIMUser" {
666 return self.create_user(call, ctx).await;
667 }
668 if call.operation_id() == "createSCIMGroup" {
669 return self.create_group(call, ctx).await;
670 }
671 if call.operation_id() == "listSCIMUsers" {
672 return self.list_users(call, ctx).await;
673 }
674 if call.operation_id() == "listSCIMGroups" {
675 return self.list_groups(call, ctx).await;
676 }
677 if call.operation_id() == "getSCIMUser" {
678 return self.get_user(call, ctx).await;
679 }
680 if call.operation_id() == "getSCIMGroup" {
681 return self.get_group(call, ctx).await;
682 }
683 if call.operation_id() == "reconcileSCIMProjection" {
684 return self.reconcile_projection(call, ctx).await;
685 }
686 if call.operation_id() == "decommissionSCIMManagedConnection" {
687 return self.decommission_managed_connection(call, ctx).await;
688 }
689 if call.operation_id() == "decommissionSCIMConnection" {
690 return self.decommission_connection(call, ctx).await;
691 }
692 if call.operation_id() == "listSCIMManagedConnectionEvents" {
693 return self.list_managed_connection_events(call, ctx).await;
694 }
695 if call.operation_id() == "revokeSCIMManagedCredential" {
696 return self.revoke_managed_credential(call, ctx).await;
697 }
698 if call.operation_id() == "rotateSCIMManagedCredential" {
699 return self.rotate_managed_credential(call, ctx).await;
700 }
701 if call.operation_id() == "getSCIMManagedConnection" {
702 return self.get_managed_connection(call, ctx).await;
703 }
704 if call.operation_id() == "listSCIMManagedConnections" {
705 return self.list_managed_connections(call, ctx).await;
706 }
707 if call.operation_id() == "createSCIMManagedConnection" {
708 return self.create_managed_connection(call, ctx).await;
709 }
710 if call.operation_id() == "getSCIMSchema" {
711 return Self::schema(
712 ctx,
713 call.params()
714 .and_then(|params| params.get("schemaId"))
715 .and_then(alibi_core::utils::json::JsValue::as_str)
716 .unwrap_or(""),
717 );
718 }
719 if call.operation_id() == "getSCIMSchemas" {
720 return Ok(EndpointResponse::json(&Self::schemas(ctx)?)?
721 .with_header("x-better-auth-scim-response", "1"));
722 }
723 if call.operation_id() == "getSCIMResourceType" {
724 return Self::resource_type(
725 ctx,
726 call.params()
727 .and_then(|params| params.get("resourceTypeId"))
728 .and_then(alibi_core::utils::json::JsValue::as_str),
729 );
730 }
731 let value = if call.operation_id() == "getSCIMResourceTypes" {
732 Self::resource_types(ctx)
733 } else {
734 Self::service_provider_config(ctx)
735 };
736 Ok(EndpointResponse::json(&value)?.with_header("x-better-auth-scim-response", "1"))
737 }
738 async fn on_request(
739 &self,
740 req: &AuthRequest,
741 ctx: &AuthContext<S>,
742 ) -> AuthResult<Option<AuthResponse>> {
743 if req.method() == &HttpMethod::Patch
744 && let Some(id) = req.path().strip_prefix("/scim/v2/Users/")
745 {
746 let call = EndpointCall::from_http_request(
747 req,
748 &AuthRoute::new(
749 HttpMethod::Patch,
750 "/scim/v2/Users/{userId}",
751 "patchSCIMUser",
752 ),
753 )
754 .with_params_value(alibi_core::utils::json::JsValue::from(json!({"userId":id})));
755 let response = if let Some(error) = resources::mutation_request_error(req, &call) {
756 error
757 } else {
758 self.patch_user(&call, ctx).await?
759 };
760 return Ok(Some(resources::to_http_response(&response)?));
761 }
762 if req.method() == &HttpMethod::Put
763 && let Some(id) = req.path().strip_prefix("/scim/v2/Users/")
764 {
765 let call = EndpointCall::from_http_request(
766 req,
767 &AuthRoute::put("/scim/v2/Users/{userId}", "replaceSCIMUser"),
768 )
769 .with_params_value(alibi_core::utils::json::JsValue::from(json!({"userId":id})));
770 let response = if let Some(error) = resources::mutation_request_error(req, &call) {
771 error
772 } else {
773 self.replace_user(&call, ctx).await?
774 };
775 return Ok(Some(resources::to_http_response(&response)?));
776 }
777 if req.method() == &HttpMethod::Delete
778 && let Some(id) = req.path().strip_prefix("/scim/v2/Users/")
779 {
780 let call = EndpointCall::from_http_request(
781 req,
782 &AuthRoute::delete("/scim/v2/Users/{userId}", "deleteSCIMUser"),
783 )
784 .with_params_value(alibi_core::utils::json::JsValue::from(json!({"userId":id})));
785 let response = self.delete_user(&call, ctx).await?;
786 return Ok(Some(resources::to_http_response(&response)?));
787 }
788 if matches!(req.method(), HttpMethod::Put | HttpMethod::Patch)
789 && let Some(id) = req.path().strip_prefix("/scim/v2/Groups/")
790 {
791 let route = if req.method() == &HttpMethod::Put {
792 AuthRoute::put("/scim/v2/Groups/{groupId}", "replaceSCIMGroup")
793 } else {
794 AuthRoute::new(
795 HttpMethod::Patch,
796 "/scim/v2/Groups/{groupId}",
797 "patchSCIMGroup",
798 )
799 };
800 let call = EndpointCall::from_http_request(req, &route).with_params_value(
801 alibi_core::utils::json::JsValue::from(json!({"groupId":id})),
802 );
803 let response = match resources::mutation_request_error(req, &call) {
804 Some(response) => response,
805 None => {
806 if req.method() == &HttpMethod::Put {
807 self.replace_group(&call, ctx).await?
808 } else {
809 self.patch_group(&call, ctx).await?
810 }
811 }
812 };
813 return Ok(Some(resources::to_http_response(&response)?));
814 }
815 if req.method() == &HttpMethod::Delete
816 && let Some(id) = req.path().strip_prefix("/scim/v2/Groups/")
817 {
818 let call = EndpointCall::from_http_request(
819 req,
820 &AuthRoute::delete("/scim/v2/Groups/{groupId}", "deleteSCIMGroup"),
821 )
822 .with_params_value(alibi_core::utils::json::JsValue::from(
823 json!({"groupId":id}),
824 ));
825 let mut response = self.delete_group(&call, ctx).await?.to_response()?;
826 let mut headers = alibi_core::Headers::new();
827 for (name, value) in response.headers {
828 if !name.eq_ignore_ascii_case("x-better-auth-scim-response") {
829 headers.append(name, value);
830 }
831 }
832 response.headers = headers;
833 return Ok(Some(
834 response.with_header("content-type", "application/scim+json"),
835 ));
836 }
837 if req.method() == &HttpMethod::Post && req.path() == "/scim/v2/Users" {
838 let call = EndpointCall::from_http_request(
839 req,
840 &AuthRoute::post("/scim/v2/Users", "createSCIMUser"),
841 );
842 let response = if let Some(error) = resources::mutation_request_error(req, &call) {
843 error
844 } else {
845 self.create_user(&call, ctx).await?
846 };
847 return Ok(Some(resources::to_http_response(&response)?));
848 }
849 if req.method() == &HttpMethod::Post && req.path() == "/scim/v2/Groups" {
850 let call = EndpointCall::from_http_request(
851 req,
852 &AuthRoute::post("/scim/v2/Groups", "createSCIMGroup"),
853 );
854 let media = req
855 .headers
856 .get("content-type")
857 .and_then(|value| value.split(';').next())
858 .map(str::trim);
859 let response = if !media.is_some_and(|media| {
860 media.eq_ignore_ascii_case("application/json")
861 || media.eq_ignore_ascii_case("application/scim+json")
862 }) {
863 resources::error(
864 415,
865 "SCIM requests must use application/scim+json or application/json",
866 None,
867 )
868 } else if call.body().is_none() {
869 resources::error(
870 400,
871 "SCIM request body must contain valid JSON",
872 Some("invalidSyntax"),
873 )
874 } else {
875 self.create_group(&call, ctx).await?
876 };
877 let mut response = response.to_response()?;
878 let mut headers = alibi_core::Headers::new();
879 for (name, value) in response.headers {
880 if !name.eq_ignore_ascii_case("x-better-auth-scim-response") {
881 headers.append(name, value);
882 }
883 }
884 response.headers = headers;
885 return Ok(Some(
886 response.with_header("content-type", "application/scim+json"),
887 ));
888 }
889 if req.method() == &HttpMethod::Get && req.path() == "/scim/v2/Users" {
890 let call = EndpointCall::from_http_request(
891 req,
892 &AuthRoute::get("/scim/v2/Users", "listSCIMUsers"),
893 );
894 let mut response = self.list_users(&call, ctx).await?.to_response()?;
895 let mut headers = alibi_core::Headers::new();
896 for (name, value) in response.headers {
897 if !name.eq_ignore_ascii_case("x-better-auth-scim-response") {
898 headers.append(name, value);
899 }
900 }
901 response.headers = headers;
902 return Ok(Some(
903 response.with_header("content-type", "application/scim+json"),
904 ));
905 }
906 if req.method() == &HttpMethod::Get && req.path() == "/scim/v2/Groups" {
907 let call = EndpointCall::from_http_request(
908 req,
909 &AuthRoute::get("/scim/v2/Groups", "listSCIMGroups"),
910 );
911 let mut response = self.list_groups(&call, ctx).await?.to_response()?;
912 let mut headers = alibi_core::Headers::new();
913 for (name, value) in response.headers {
914 if !name.eq_ignore_ascii_case("x-better-auth-scim-response") {
915 headers.append(name, value);
916 }
917 }
918 response.headers = headers;
919 return Ok(Some(
920 response.with_header("content-type", "application/scim+json"),
921 ));
922 }
923 if req.method() == &HttpMethod::Get
924 && let Some(id) = req.path().strip_prefix("/scim/v2/Users/")
925 {
926 let call = EndpointCall::from_http_request(
927 req,
928 &AuthRoute::get("/scim/v2/Users/{userId}", "getSCIMUser"),
929 )
930 .with_params_value(alibi_core::utils::json::JsValue::from(json!({"userId":id})));
931 let mut response = self.get_user(&call, ctx).await?.to_response()?;
932 let mut headers = alibi_core::Headers::new();
933 for (name, value) in response.headers {
934 if !name.eq_ignore_ascii_case("x-better-auth-scim-response") {
935 headers.append(name, value);
936 }
937 }
938 response.headers = headers;
939 return Ok(Some(
940 response.with_header("content-type", "application/scim+json"),
941 ));
942 }
943 if req.method() == &HttpMethod::Get
944 && let Some(id) = req.path().strip_prefix("/scim/v2/Groups/")
945 {
946 let call = EndpointCall::from_http_request(
947 req,
948 &AuthRoute::get("/scim/v2/Groups/{groupId}", "getSCIMGroup"),
949 )
950 .with_params_value(alibi_core::utils::json::JsValue::from(
951 json!({"groupId":id}),
952 ));
953 let mut response = self.get_group(&call, ctx).await?.to_response()?;
954 let mut headers = alibi_core::Headers::new();
955 for (name, value) in response.headers {
956 if !name.eq_ignore_ascii_case("x-better-auth-scim-response") {
957 headers.append(name, value);
958 }
959 }
960 response.headers = headers;
961 return Ok(Some(
962 response.with_header("content-type", "application/scim+json"),
963 ));
964 }
965 if req.method() == &HttpMethod::Get
966 && let Some(id) = req.path().strip_prefix("/scim/v2/Schemas/")
967 {
968 return Ok(Some(resources::to_http_response(&Self::schema(ctx, id)?)?));
969 }
970 if req.method() == &HttpMethod::Get
971 && let Some(id) = req.path().strip_prefix("/scim/v2/ResourceTypes/")
972 {
973 return Ok(Some(resources::to_http_response(&Self::resource_type(
974 ctx,
975 Some(id),
976 )?)?));
977 }
978 if req.method() != &HttpMethod::Get
979 || !matches!(
980 req.path(),
981 "/scim/v2/ServiceProviderConfig" | "/scim/v2/ResourceTypes" | "/scim/v2/Schemas"
982 )
983 {
984 return Ok(None);
985 }
986 let value = if req.path() == "/scim/v2/Schemas" {
987 Self::schemas(ctx)?
988 } else if req.path() == "/scim/v2/ResourceTypes" {
989 Self::resource_types(ctx)
990 } else {
991 Self::service_provider_config(ctx)
992 };
993 Ok(Some(
994 AuthResponse::json(200, &value)?.with_header("content-type", "application/scim+json"),
995 ))
996 }
997}