kanidm_proto/scim_v1/
client.rs

1//! These are types that a client will send to the server.
2use super::{ScimEntryGeneric, ScimEntryGetQuery, ScimMail, ScimOauth2ClaimMapJoinChar};
3use crate::attribute::Attribute;
4use crate::v1::OutboundMessage;
5use scim_proto::ScimEntryHeader;
6use serde::{Deserialize, Serialize};
7use serde_json::Value as JsonValue;
8use serde_with::formats::PreferMany;
9use serde_with::OneOrMany;
10use serde_with::{base64, formats, serde_as, skip_serializing_none};
11use sshkey_attest::proto::PublicKey as SshPublicKey;
12use std::collections::{BTreeMap, BTreeSet};
13use std::num::NonZeroU64;
14use time::format_description::well_known::Rfc3339;
15use time::OffsetDateTime;
16use utoipa::ToSchema;
17use uuid::Uuid;
18
19pub type ScimSshPublicKeys = Vec<ScimSshPublicKey>;
20
21#[derive(Deserialize, Serialize, Debug, Clone)]
22#[serde(deny_unknown_fields, rename_all = "camelCase")]
23pub struct ScimSshPublicKey {
24    pub label: String,
25    pub value: SshPublicKey,
26}
27
28#[derive(Deserialize)]
29#[serde(untagged)]
30enum ScimReferenceAdapter {
31    Complete { uuid: Uuid, value: String },
32    Uuid { uuid: Uuid },
33    UuidX(Uuid),
34    Value { value: String },
35    ValueX(String),
36}
37
38impl From<ScimReferenceAdapter> for ScimReference {
39    fn from(scr: ScimReferenceAdapter) -> Self {
40        match scr {
41            ScimReferenceAdapter::Complete { uuid, value } => ScimReference {
42                uuid: Some(uuid),
43                value: Some(value),
44            },
45            ScimReferenceAdapter::Uuid { uuid } | ScimReferenceAdapter::UuidX(uuid) => {
46                ScimReference {
47                    uuid: Some(uuid),
48                    value: None,
49                }
50            }
51            ScimReferenceAdapter::Value { value } | ScimReferenceAdapter::ValueX(value) => {
52                ScimReference {
53                    uuid: None,
54                    value: Some(value),
55                }
56            }
57        }
58    }
59}
60
61#[serde_as]
62#[skip_serializing_none]
63#[derive(Deserialize, Serialize, Debug, Clone)]
64#[serde(
65    deny_unknown_fields,
66    rename_all = "camelCase",
67    from = "ScimReferenceAdapter"
68)]
69pub struct ScimReference {
70    pub uuid: Option<Uuid>,
71    pub value: Option<String>,
72}
73
74impl<T> From<T> for ScimReference
75where
76    T: AsRef<str>,
77{
78    fn from(value: T) -> Self {
79        ScimReference {
80            uuid: None,
81            value: Some(value.as_ref().to_string()),
82        }
83    }
84}
85
86pub type ScimReferences = Vec<ScimReference>;
87
88#[serde_as]
89#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
90#[serde(transparent)]
91pub struct ScimDateTime {
92    #[serde_as(as = "Rfc3339")]
93    pub date_time: OffsetDateTime,
94}
95
96#[serde_as]
97#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
98#[serde(rename_all = "camelCase")]
99pub struct ScimCertificate {
100    #[serde_as(as = "base64::Base64<base64::UrlSafe, formats::Unpadded>")]
101    pub der: Vec<u8>,
102}
103
104#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
105#[serde(rename_all = "camelCase")]
106pub struct ScimAddress {
107    pub street_address: String,
108    pub locality: String,
109    pub region: String,
110    pub postal_code: String,
111    pub country: String,
112}
113
114#[serde_as]
115#[derive(Deserialize, Serialize, Debug, Clone)]
116#[serde(rename_all = "camelCase")]
117pub struct ScimOAuth2ClaimMap {
118    pub group: Option<String>,
119    pub group_uuid: Option<Uuid>,
120    pub claim: String,
121    pub join_char: ScimOauth2ClaimMapJoinChar,
122    pub values: BTreeSet<String>,
123}
124
125#[serde_as]
126#[derive(Deserialize, Serialize, Debug, Clone)]
127#[serde(rename_all = "camelCase")]
128pub struct ScimOAuth2ScopeMap {
129    pub group: Option<String>,
130    pub group_uuid: Option<Uuid>,
131    pub scopes: BTreeSet<String>,
132}
133
134#[serde_as]
135#[derive(Serialize, Deserialize, Clone, Debug)]
136#[serde(rename_all = "camelCase")]
137pub struct ScimListEntry {
138    pub schemas: Vec<String>,
139    pub total_results: u64,
140    pub items_per_page: Option<NonZeroU64>,
141    pub start_index: Option<NonZeroU64>,
142    pub resources: Vec<ScimEntryGeneric>,
143}
144
145#[serde_as]
146#[derive(Serialize, Debug, Clone)]
147#[serde(rename_all = "snake_case")]
148pub struct ScimEntryApplicationPost {
149    pub name: String,
150    pub displayname: String,
151    pub linked_group: ScimReference,
152}
153
154#[serde_as]
155#[derive(Deserialize, Debug, Clone)]
156#[serde(rename_all = "snake_case")]
157pub struct ScimEntryApplication {
158    #[serde(flatten)]
159    pub header: ScimEntryHeader,
160
161    pub name: String,
162    pub displayname: String,
163
164    pub linked_group: Vec<super::ScimReference>,
165
166    #[serde(flatten)]
167    pub attrs: BTreeMap<Attribute, JsonValue>,
168}
169
170#[serde_as]
171#[derive(Deserialize, Clone, Debug)]
172#[serde(rename_all = "camelCase")]
173pub struct ScimListApplication {
174    pub schemas: Vec<String>,
175    pub total_results: u64,
176    pub items_per_page: Option<NonZeroU64>,
177    pub start_index: Option<NonZeroU64>,
178    pub resources: Vec<ScimEntryApplication>,
179}
180
181#[serde_as]
182#[derive(Serialize, Deserialize, Debug, Clone)]
183#[serde(rename_all = "snake_case")]
184pub struct ScimEntryMessage {
185    #[serde(flatten)]
186    pub header: ScimEntryHeader,
187
188    pub message_template: OutboundMessage,
189    pub send_after: ScimDateTime,
190    pub delete_after: ScimDateTime,
191    pub sent_at: Option<ScimDateTime>,
192    pub mail_destination: Vec<ScimMail>,
193
194    #[serde(flatten)]
195    pub attrs: BTreeMap<Attribute, JsonValue>,
196}
197
198#[serde_as]
199#[derive(Serialize, Deserialize, Clone, Debug)]
200#[serde(rename_all = "camelCase")]
201pub struct ScimListMessage {
202    pub schemas: Vec<String>,
203    pub total_results: u64,
204    pub items_per_page: Option<NonZeroU64>,
205    pub start_index: Option<NonZeroU64>,
206    pub resources: Vec<ScimEntryMessage>,
207}
208
209#[serde_as]
210#[derive(Serialize, Deserialize, Debug, Clone)]
211#[serde(rename_all = "snake_case")]
212pub struct ScimEntrySchemaClass {
213    #[serde(flatten)]
214    pub header: ScimEntryHeader,
215
216    // pub name: String,
217    // pub displayname: String,
218    // pub linked_group: Vec<super::ScimReference>,
219    #[serde(flatten)]
220    pub attrs: BTreeMap<Attribute, JsonValue>,
221}
222
223#[serde_as]
224#[derive(Deserialize, Clone, Debug)]
225#[serde(rename_all = "camelCase")]
226pub struct ScimListSchemaClass {
227    pub schemas: Vec<String>,
228    pub total_results: u64,
229    pub items_per_page: Option<NonZeroU64>,
230    pub start_index: Option<NonZeroU64>,
231    pub resources: Vec<ScimEntrySchemaClass>,
232}
233
234#[serde_as]
235#[derive(Deserialize, Debug, Clone, Serialize)]
236#[serde(rename_all = "snake_case")]
237pub struct ScimEntrySchemaAttribute {
238    #[serde(flatten)]
239    pub header: ScimEntryHeader,
240
241    pub attributename: String,
242    pub description: String,
243    // TODO: To be removed
244    pub multivalue: bool,
245    pub unique: bool,
246    pub syntax: String,
247    // pub linked_group: Vec<super::ScimReference>,
248    #[serde(flatten)]
249    pub attrs: BTreeMap<Attribute, JsonValue>,
250}
251
252#[serde_as]
253#[derive(Deserialize, Clone, Debug)]
254#[serde(rename_all = "camelCase")]
255pub struct ScimListSchemaAttribute {
256    pub schemas: Vec<String>,
257    pub total_results: u64,
258    pub items_per_page: Option<NonZeroU64>,
259    pub start_index: Option<NonZeroU64>,
260    pub resources: Vec<ScimEntrySchemaAttribute>,
261}
262
263#[derive(Serialize, Debug, Clone)]
264pub struct ScimEntryPutKanidm {
265    pub id: Uuid,
266    #[serde(flatten)]
267    pub attrs: BTreeMap<Attribute, Option<super::server::ScimValueKanidm>>,
268}
269
270#[serde_as]
271#[derive(Deserialize, Serialize, Debug, Clone)]
272pub struct ScimStrings(#[serde_as(as = "OneOrMany<_, PreferMany>")] pub Vec<String>);
273
274#[derive(Debug, Clone, Serialize, Deserialize, Default, ToSchema)]
275pub struct ScimEntryPostGeneric {
276    /// Create an attribute to contain the following value state.
277    #[serde(flatten)]
278    #[schema(value_type = Object, additional_properties = true)]
279    pub attrs: BTreeMap<Attribute, JsonValue>,
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
283#[serde(rename_all = "lowercase", tag = "state")]
284pub enum ScimEntryAssertion {
285    /// The entry should be present, with this id/UUID, and
286    /// the content of these attributes must be as shown. If an
287    /// attribute is not present in the assertion, it will not be
288    /// altered. To remove an attribute, set the attribute to "null".
289    Present {
290        id: Uuid,
291        #[schema(value_type = BTreeMap<String, Value>)]
292        #[serde(flatten)]
293        attrs: BTreeMap<Attribute, Option<JsonValue>>,
294    },
295    /// The entry should be absent (removed) from the database. Once
296    /// removed, the entry can not be re-asserted. You will need to create
297    /// a new entry with a unique ID.
298    Absent { id: Uuid },
299}
300
301#[derive(Debug, Clone, Serialize, Deserialize, Default, ToSchema)]
302pub struct ScimAssertGeneric {
303    /// The ID of this assertion.
304    pub id: Uuid,
305
306    /// A set of assertions about expected entry state.
307    pub assertions: Vec<ScimEntryAssertion>,
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize, Default, ToSchema)]
311pub struct ScimEntryPutGeneric {
312    // id is only used to target the entry in question
313    pub id: Uuid,
314
315    #[serde(flatten)]
316    /// Non-standard extension - allow query options to be set in a put request. This
317    /// is because a put request also returns the entry state post put, so we want
318    /// to allow putters to adjust and control what is returned here.
319    pub query: ScimEntryGetQuery,
320
321    // external_id can't be set by put
322    // meta is skipped on put
323    // Schemas are decoded as part of "attrs".
324    /// Update an attribute to contain the following value state.
325    /// If the attribute is None, it is removed.
326    #[schema(value_type = BTreeMap<String, Value>)]
327    #[serde(flatten)]
328    pub attrs: BTreeMap<Attribute, Option<JsonValue>>,
329}
330
331impl TryFrom<ScimEntryPutKanidm> for ScimEntryPutGeneric {
332    type Error = serde_json::Error;
333
334    fn try_from(value: ScimEntryPutKanidm) -> Result<Self, Self::Error> {
335        let ScimEntryPutKanidm { id, attrs } = value;
336
337        let attrs = attrs
338            .into_iter()
339            .map(|(attr, value)| {
340                if let Some(v) = value {
341                    serde_json::to_value(v).map(|json_value| (attr, Some(json_value)))
342                } else {
343                    Ok((attr, None))
344                }
345            })
346            .collect::<Result<_, _>>()?;
347
348        Ok(ScimEntryPutGeneric {
349            id,
350            attrs,
351            query: Default::default(),
352        })
353    }
354}