Skip to main content

rustauth_scim/
resources.rs

1//! SCIM resource mapping.
2
3use std::collections::BTreeMap;
4
5use rustauth_core::db::{Account, User};
6use serde::{Deserialize, Serialize};
7use time::OffsetDateTime;
8
9use crate::mappings::resource_url;
10use crate::metadata::{SCIM_GROUP_SCHEMA_ID, SCIM_USER_SCHEMA_ID};
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "camelCase")]
14pub struct ScimUserResource {
15    pub id: String,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub external_id: Option<String>,
18    pub meta: ScimResourceMeta,
19    #[serde(rename = "userName")]
20    pub user_name: String,
21    pub name: ScimUserResourceName,
22    #[serde(rename = "displayName")]
23    pub display_name: String,
24    pub active: bool,
25    pub emails: Vec<ScimUserResourceEmail>,
26    #[serde(skip_serializing_if = "Vec::is_empty", default)]
27    pub groups: Vec<ScimUserResourceGroup>,
28    pub schemas: Vec<String>,
29    #[serde(flatten, skip_serializing_if = "BTreeMap::is_empty", default)]
30    pub additional_fields: BTreeMap<String, serde_json::Value>,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub struct ScimUserResourceName {
35    pub formatted: String,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39pub struct ScimUserResourceEmail {
40    pub primary: bool,
41    pub value: String,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45pub struct ScimUserResourceGroup {
46    pub value: String,
47    #[serde(rename = "$ref")]
48    pub ref_: String,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub display: Option<String>,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54#[serde(rename_all = "camelCase")]
55pub struct ScimResourceMeta {
56    pub resource_type: String,
57    #[serde(with = "time::serde::rfc3339")]
58    pub created: OffsetDateTime,
59    #[serde(with = "time::serde::rfc3339")]
60    pub last_modified: OffsetDateTime,
61    pub location: String,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub version: Option<String>,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67#[serde(rename_all = "camelCase")]
68pub struct ScimGroupResource {
69    pub id: String,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub external_id: Option<String>,
72    pub meta: ScimResourceMeta,
73    #[serde(rename = "displayName")]
74    pub display_name: String,
75    #[serde(skip_serializing_if = "Vec::is_empty", default)]
76    pub members: Vec<ScimGroupResourceMember>,
77    pub schemas: Vec<String>,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81pub struct ScimGroupResourceMember {
82    pub value: String,
83    #[serde(rename = "$ref")]
84    pub ref_: String,
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub display: Option<String>,
87}
88
89pub fn user_resource(base_url: &str, user: &User, account: Option<&Account>) -> ScimUserResource {
90    ScimUserResource {
91        id: user.id.clone(),
92        external_id: account.map(|account| account.account_id.clone()),
93        meta: ScimResourceMeta {
94            resource_type: "User".to_owned(),
95            created: user.created_at,
96            last_modified: user.updated_at,
97            location: resource_url(base_url, &format!("/scim/v2/Users/{}", user.id)),
98            version: Some(resource_version(user.updated_at)),
99        },
100        user_name: user.email.clone(),
101        name: ScimUserResourceName {
102            formatted: user.name.clone(),
103        },
104        display_name: user.name.clone(),
105        active: true,
106        emails: vec![ScimUserResourceEmail {
107            primary: true,
108            value: user.email.clone(),
109        }],
110        groups: Vec::new(),
111        schemas: vec![SCIM_USER_SCHEMA_ID.to_owned()],
112        additional_fields: BTreeMap::new(),
113    }
114}
115
116pub fn group_resource(
117    base_url: &str,
118    group_id: &str,
119    external_id: Option<String>,
120    display_name: String,
121    created_at: OffsetDateTime,
122    updated_at: OffsetDateTime,
123    members: Vec<ScimGroupResourceMember>,
124) -> ScimGroupResource {
125    ScimGroupResource {
126        id: group_id.to_owned(),
127        external_id,
128        meta: ScimResourceMeta {
129            resource_type: "Group".to_owned(),
130            created: created_at,
131            last_modified: updated_at,
132            location: resource_url(base_url, &format!("/scim/v2/Groups/{group_id}")),
133            version: Some(resource_version(updated_at)),
134        },
135        display_name,
136        members,
137        schemas: vec![SCIM_GROUP_SCHEMA_ID.to_owned()],
138    }
139}
140
141pub fn group_member_resource(
142    base_url: &str,
143    user_id: &str,
144    display: Option<String>,
145) -> ScimGroupResourceMember {
146    ScimGroupResourceMember {
147        value: user_id.to_owned(),
148        ref_: resource_url(base_url, &format!("/scim/v2/Users/{user_id}")),
149        display,
150    }
151}
152
153pub fn resource_version(updated_at: OffsetDateTime) -> String {
154    format!(r#"W/"{}""#, updated_at.unix_timestamp_nanos())
155}