Skip to main content

alibi_core/store/
sso.rs

1//! Durable enterprise identity-provider configuration.
2use crate::{AuthError, AuthResult};
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
6#[serde(rename_all = "camelCase")]
7pub struct SsoProvider {
8    pub id: String,
9    pub issuer: String,
10    pub domain: String,
11    pub provider_id: String,
12    pub user_id: Option<String>,
13    pub organization_id: Option<String>,
14    pub oidc_config: Option<String>,
15    pub saml_config: Option<String>,
16    pub domain_verified: Option<bool>,
17    #[serde(flatten, default)]
18    pub additional_fields: serde_json::Map<String, serde_json::Value>,
19}
20/// A provider update prepared from the actual row locked by its adapter.
21pub struct SsoProviderUpdate {
22    pub provider: SsoProvider,
23    pub identity_changed: bool,
24    /// Only submitted fields are bound again during partial updates.
25    pub additional_field_changes: serde_json::Map<String, serde_json::Value>,
26}
27pub type SsoProviderUpdateWork =
28    dyn Fn(&SsoProvider) -> AuthResult<SsoProviderUpdate> + Send + Sync;
29#[async_trait]
30pub trait SsoProviderStore: Send + Sync {
31    async fn mark_sso_domain_verified(&self, _id: &str, _domain: &str) -> AuthResult<bool> {
32        Err(AuthError::config(
33            "SSO persistence is not supported by this store",
34        ))
35    }
36    async fn update_sso_provider(
37        &self,
38        _id: &str,
39        _provider_id: &str,
40        _prepare: &SsoProviderUpdateWork,
41    ) -> AuthResult<Option<SsoProvider>> {
42        Err(AuthError::config(
43            "SSO persistence is not supported by this store",
44        ))
45    }
46    /// Atomically retire a provider and every account linked to its identity namespace.
47    async fn delete_sso_provider_and_accounts(
48        &self,
49        _id: &str,
50        _provider_id: &str,
51    ) -> AuthResult<bool> {
52        Err(AuthError::config(
53            "SSO persistence is not supported by this store",
54        ))
55    }
56    async fn list_sso_providers(&self) -> AuthResult<Vec<SsoProvider>> {
57        Err(AuthError::config(
58            "SSO persistence is not supported by this store",
59        ))
60    }
61    async fn create_sso_provider(&self, _provider: SsoProvider) -> AuthResult<SsoProvider> {
62        Err(AuthError::config(
63            "SSO persistence is not supported by this store",
64        ))
65    }
66    async fn get_sso_provider(&self, _provider_id: &str) -> AuthResult<Option<SsoProvider>> {
67        Err(AuthError::config(
68            "SSO persistence is not supported by this store",
69        ))
70    }
71    async fn list_sso_providers_by_user(&self, _user_id: &str) -> AuthResult<Vec<SsoProvider>> {
72        Err(AuthError::config(
73            "SSO persistence is not supported by this store",
74        ))
75    }
76}
77
78/// Physical storage category of a declared provider field.
79#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80pub enum SsoFieldType {
81    String,
82    Boolean,
83    Number,
84    Json,
85    Date,
86}
87impl SsoFieldType {
88    #[must_use]
89    pub fn from_config(field: &crate::field_policy::FieldConfig) -> Self {
90        match field.schema.get("type").and_then(serde_json::Value::as_str) {
91            Some("boolean") => Self::Boolean,
92            Some("number" | "integer") => Self::Number,
93            Some("object" | "array" | "json" | "string[]" | "number[]") => Self::Json,
94            Some("date") => Self::Date,
95            _ => Self::String,
96        }
97    }
98    #[must_use]
99    pub const fn sql_type(self, postgres: bool) -> &'static str {
100        match self {
101            Self::String => "TEXT",
102            Self::Boolean => {
103                if postgres {
104                    "BOOLEAN"
105                } else {
106                    "INTEGER"
107                }
108            }
109            Self::Number => "INTEGER",
110            Self::Json => {
111                if postgres {
112                    "JSONB"
113                } else {
114                    "TEXT"
115                }
116            }
117            Self::Date => {
118                if postgres {
119                    "TIMESTAMPTZ"
120                } else {
121                    "DATE"
122                }
123            }
124        }
125    }
126}
127/// Resolve physical additional-column names without changing logical API names.
128#[must_use]
129pub fn sso_field_column<'a>(name: &'a str, field: &'a crate::field_policy::FieldConfig) -> &'a str {
130    field.field_name.as_deref().unwrap_or(name)
131}
132
133/// Validate additional-field names against provider columns and returned fields.
134pub fn validate_sso_fields(config: &crate::config::AdvancedDatabaseConfig) -> AuthResult<()> {
135    const BUILT_IN: &[&str] = &[
136        "id",
137        "issuer",
138        "domain",
139        "providerId",
140        "userId",
141        "organizationId",
142        "oidcConfig",
143        "samlConfig",
144        "domainVerified",
145    ];
146    const RESPONSE: &[&str] = &[
147        "type",
148        "redirectURI",
149        "domainVerificationToken",
150        "spMetadataUrl",
151    ];
152    let mapped = config.sso_provider.as_ref();
153    for (name, field) in &config.sso_provider_fields.0 {
154        if BUILT_IN.contains(&name.as_str()) {
155            return Err(AuthError::config(format!(
156                "ssoProvider additional field \"{name}\" conflicts with a built-in field"
157            )));
158        }
159        if RESPONSE.contains(&name.as_str()) {
160            return Err(AuthError::config(format!(
161                "ssoProvider additional field \"{name}\" conflicts with a returned provider field"
162            )));
163        }
164        let physical = sso_field_column(name, field);
165        if physical.is_empty()
166            || BUILT_IN.iter().any(|canonical| {
167                let configured = mapped
168                    .and_then(|mapping| {
169                        mapping
170                            .columns
171                            .iter()
172                            .find(|(key, _)| crate::config::sso_provider_column(key) == *canonical)
173                            .map(|(_, value)| value.as_str())
174                    })
175                    .unwrap_or(canonical);
176                configured == physical
177            })
178        {
179            return Err(AuthError::config(format!(
180                "ssoProvider additional field \"{name}\" maps to built-in field \"{physical}\""
181            )));
182        }
183    }
184    Ok(())
185}
186/// DDL for one declared column. Application callback defaults are evaluated at creation.
187pub fn sso_field_definition(
188    name: &str,
189    field: &crate::field_policy::FieldConfig,
190    postgres: bool,
191) -> AuthResult<String> {
192    use crate::field_policy::FieldDefault;
193    let quote = |value: &str| format!("\"{}\"", value.replace('"', "\"\""));
194    let mut definition = format!(
195        "{} {}",
196        quote(sso_field_column(name, field)),
197        SsoFieldType::from_config(field).sql_type(postgres)
198    );
199    if let Some(FieldDefault::Value(value)) = &field.default {
200        let default = match value {
201            crate::utils::json::JsValue::Null => "NULL".to_owned(),
202            crate::utils::json::JsValue::Bool(value) => {
203                if postgres {
204                    value.to_string()
205                } else {
206                    if *value { "1" } else { "0" }.to_owned()
207                }
208            }
209            crate::utils::json::JsValue::Number(value) if value.is_finite() => value.to_string(),
210            crate::utils::json::JsValue::String(value) => {
211                format!("'{}'", value.replace('\'', "''"))
212            }
213            value => format!("'{}'", serde_json::to_string(value)?.replace('\'', "''")),
214        };
215        definition.push_str(" DEFAULT ");
216        definition.push_str(&default);
217    }
218    if field.required {
219        definition.push_str(" NOT NULL");
220    }
221    Ok(definition)
222}
223
224/// Apply application adapter input callbacks and defaults immediately before binding.
225pub async fn prepare_sso_fields(
226    fields: &crate::field_policy::SessionFields,
227    input: &serde_json::Map<String, serde_json::Value>,
228    create: bool,
229) -> AuthResult<serde_json::Map<String, serde_json::Value>> {
230    let policy = crate::field_policy::SessionAdapterFields(std::sync::Arc::new(fields.0.clone()));
231    let mut values: crate::field_policy::FieldValues = input
232        .iter()
233        .map(|(key, value)| {
234            (
235                key.clone(),
236                crate::utils::json::JsValue::from(value.clone()),
237            )
238        })
239        .collect();
240    policy.attach(&mut values, create);
241    values.apply_adapter_transforms_async().await?;
242    values
243        .into_iter()
244        .map(|(key, value)| Ok((key, value.to_json_value()?)))
245        .collect()
246}
247/// Resolve application output callbacks on the actual stored provider projection.
248pub async fn project_sso_provider(
249    fields: &crate::field_policy::SessionFields,
250    mut provider: SsoProvider,
251) -> AuthResult<SsoProvider> {
252    let policy = crate::field_policy::SessionAdapterFields(std::sync::Arc::new(fields.0.clone()));
253    let output = policy
254        .output(std::mem::take(&mut provider.additional_fields))
255        .await?;
256    provider.additional_fields.clone_from(output.values());
257    // The reference normalizes database date strings after output transforms.
258    for (name, field) in &fields.0 {
259        if SsoFieldType::from_config(field) == SsoFieldType::Date
260            && let Some(value) = provider.additional_fields.get_mut(name)
261            && let Some(text) = value.as_str()
262        {
263            *value = chrono::DateTime::parse_from_rfc3339(text).map_or(
264                serde_json::Value::Null,
265                |date| {
266                    serde_json::Value::String(
267                        date.with_timezone(&chrono::Utc)
268                            .to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
269                    )
270                },
271            );
272        }
273    }
274    Ok(provider)
275}