1use 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}
20pub struct SsoProviderUpdate {
22 pub provider: SsoProvider,
23 pub identity_changed: bool,
24 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 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#[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#[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
133pub 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}
186pub 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
224pub 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}
247pub 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 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}