rustauth-scim 0.3.0

SCIM support for RustAuth.
Documentation
//! SCIM resource mapping.

use std::collections::BTreeMap;

use rustauth_core::db::{Account, User};
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;

use crate::mappings::resource_url;
use crate::metadata::{SCIM_GROUP_SCHEMA_ID, SCIM_USER_SCHEMA_ID};

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ScimUserResource {
    pub id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub external_id: Option<String>,
    pub meta: ScimResourceMeta,
    #[serde(rename = "userName")]
    pub user_name: String,
    pub name: ScimUserResourceName,
    #[serde(rename = "displayName")]
    pub display_name: String,
    pub active: bool,
    pub emails: Vec<ScimUserResourceEmail>,
    #[serde(skip_serializing_if = "Vec::is_empty", default)]
    pub groups: Vec<ScimUserResourceGroup>,
    pub schemas: Vec<String>,
    #[serde(flatten, skip_serializing_if = "BTreeMap::is_empty", default)]
    pub additional_fields: BTreeMap<String, serde_json::Value>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ScimUserResourceName {
    pub formatted: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ScimUserResourceEmail {
    pub primary: bool,
    pub value: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ScimUserResourceGroup {
    pub value: String,
    #[serde(rename = "$ref")]
    pub ref_: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub display: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ScimResourceMeta {
    pub resource_type: String,
    #[serde(with = "time::serde::rfc3339")]
    pub created: OffsetDateTime,
    #[serde(with = "time::serde::rfc3339")]
    pub last_modified: OffsetDateTime,
    pub location: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub version: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ScimGroupResource {
    pub id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub external_id: Option<String>,
    pub meta: ScimResourceMeta,
    #[serde(rename = "displayName")]
    pub display_name: String,
    #[serde(skip_serializing_if = "Vec::is_empty", default)]
    pub members: Vec<ScimGroupResourceMember>,
    pub schemas: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ScimGroupResourceMember {
    pub value: String,
    #[serde(rename = "$ref")]
    pub ref_: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub display: Option<String>,
}

pub fn user_resource(base_url: &str, user: &User, account: Option<&Account>) -> ScimUserResource {
    ScimUserResource {
        id: user.id.clone(),
        external_id: account.map(|account| account.account_id.clone()),
        meta: ScimResourceMeta {
            resource_type: "User".to_owned(),
            created: user.created_at,
            last_modified: user.updated_at,
            location: resource_url(base_url, &format!("/scim/v2/Users/{}", user.id)),
            version: Some(resource_version(user.updated_at)),
        },
        user_name: user.email.clone(),
        name: ScimUserResourceName {
            formatted: user.name.clone(),
        },
        display_name: user.name.clone(),
        active: true,
        emails: vec![ScimUserResourceEmail {
            primary: true,
            value: user.email.clone(),
        }],
        groups: Vec::new(),
        schemas: vec![SCIM_USER_SCHEMA_ID.to_owned()],
        additional_fields: BTreeMap::new(),
    }
}

pub fn group_resource(
    base_url: &str,
    group_id: &str,
    external_id: Option<String>,
    display_name: String,
    created_at: OffsetDateTime,
    updated_at: OffsetDateTime,
    members: Vec<ScimGroupResourceMember>,
) -> ScimGroupResource {
    ScimGroupResource {
        id: group_id.to_owned(),
        external_id,
        meta: ScimResourceMeta {
            resource_type: "Group".to_owned(),
            created: created_at,
            last_modified: updated_at,
            location: resource_url(base_url, &format!("/scim/v2/Groups/{group_id}")),
            version: Some(resource_version(updated_at)),
        },
        display_name,
        members,
        schemas: vec![SCIM_GROUP_SCHEMA_ID.to_owned()],
    }
}

pub fn group_member_resource(
    base_url: &str,
    user_id: &str,
    display: Option<String>,
) -> ScimGroupResourceMember {
    ScimGroupResourceMember {
        value: user_id.to_owned(),
        ref_: resource_url(base_url, &format!("/scim/v2/Users/{user_id}")),
        display,
    }
}

pub fn resource_version(updated_at: OffsetDateTime) -> String {
    format!(r#"W/"{}""#, updated_at.unix_timestamp_nanos())
}