Skip to main content

alibi_plugins/sso/
provider_update.rs

1//! Merge partial provider configuration against its transaction-locked snapshot.
2use super::{
3    AuthContext, AuthResult, AuthSchema, AuthUser, EndpointResponse, JsValue, ServerEndpoint,
4    SessionRequest, SsoPlugin, SsoProvider, Value, api, json,
5};
6use alibi_core::store::SsoProviderUpdate;
7impl SsoPlugin {
8    pub fn update_sso_provider_endpoint(input: &Value) -> AuthResult<ServerEndpoint<Value>> {
9        ServerEndpoint::new("sso", "updateSSOProvider").with_body(input)
10    }
11    pub(super) async fn update<S: AuthSchema>(
12        &self,
13        input: &JsValue,
14        request: &impl SessionRequest,
15        ctx: &AuthContext<S>,
16    ) -> AuthResult<EndpointResponse> {
17        let (user, _) = super::super::helpers::ordinary_session(request, ctx).await?;
18        let parsed = super::additional_fields::parse(
19            &ctx.config.advanced.database.sso_provider_fields,
20            input,
21            false,
22        )?;
23        if !parsed.has_input_fields
24            && !["issuer", "domain", "samlConfig", "oidcConfig"]
25                .iter()
26                .any(|field| input.get(field).is_some_and(JsValue::is_truthy))
27        {
28            return Err(api(400, "No fields provided for update"));
29        }
30        let extras = parsed.values;
31        let provider_id = input
32            .get("providerId")
33            .and_then(JsValue::as_str)
34            .ok_or_else(|| super::super::endpoint::validation("Invalid providerId"))?;
35        let authorized = self
36            .authorize_provider_id(provider_id, user.id().as_ref(), ctx)
37            .await?;
38        let body = input.to_json_value()?;
39        if let Some(config) = body.get("oidcConfig") {
40            super::discovery::validate_endpoints(config, &|url| {
41                ctx.config.is_redirect_target_trusted(url)
42            })?;
43        }
44        let max_metadata_size = self.config.saml_max_metadata_size;
45        let private_key_configured = self.private_key_source_configured(provider_id);
46        let signing_policy = self.config.saml_signing_policy.clone();
47        let prepare = move |existing: &SsoProvider| {
48            let mut update = prepare_update(
49                existing,
50                &body,
51                max_metadata_size,
52                private_key_configured,
53                &signing_policy,
54            )?;
55            update.provider.additional_fields.extend(extras.clone());
56            update.additional_field_changes.clone_from(&extras);
57            Ok(update)
58        };
59        let guard = self
60            .config
61            .guard_provider_mutation
62            .as_ref()
63            .map(super::SsoMutationGuard::resolve::<S>)
64            .transpose()?;
65        let updated = if let Some(guard) = guard {
66            let id = authorized.id.clone();
67            let provider_id = provider_id.to_owned();
68            let verified = self.config.domain_verification;
69            alibi_core::store::transaction(ctx.database.as_ref(), move |database| {
70                Box::pin(async move {
71                    let existing = database
72                        .lock_sso_provider(&id, &provider_id)
73                        .await?
74                        .ok_or_else(|| api(404, "Provider not found"))?;
75                    let update = prepare(&existing)?;
76                    super::mutation_guard::run(
77                        guard.as_ref(),
78                        &existing,
79                        super::SsoMutationAction::Update,
80                        Some(update.identity_changed),
81                        verified,
82                        database,
83                    )
84                    .await?;
85                    database
86                        .update_sso_provider(update)
87                        .await?
88                        .ok_or_else(|| api(404, "Provider not found"))
89                })
90            })
91            .await?
92        } else {
93            ctx.database
94                .update_sso_provider(&authorized.id, provider_id, &prepare)
95                .await?
96                .ok_or_else(|| api(404, "Provider not found"))?
97        };
98        EndpointResponse::json(&super::provider_read::sanitize(&updated, ctx)?)
99    }
100}
101fn prepare_update(
102    existing: &SsoProvider,
103    body: &Value,
104    max_metadata_size: usize,
105    private_key_configured: bool,
106    signing_policy: &super::SsoSamlSigningPolicy,
107) -> AuthResult<SsoProviderUpdate> {
108    let mut provider = existing.clone();
109    let mut identity_changed = false;
110    if let Some(issuer) = body.get("issuer").and_then(Value::as_str) {
111        identity_changed = issuer != existing.issuer;
112        provider.issuer = issuer.into();
113    }
114    if let Some(domain) = body.get("domain").and_then(Value::as_str) {
115        if domain != existing.domain {
116            provider.domain_verified = Some(false);
117        }
118        provider.domain = domain.into();
119    }
120    if let Some(updates) = body.get("samlConfig") {
121        super::signing_algorithms::validate(updates, signing_policy)?;
122        let issuer = body
123            .get("issuer")
124            .and_then(Value::as_str)
125            .filter(|issuer| !issuer.is_empty());
126        let (config, changed) =
127            super::provider_update_saml::prepare(existing, updates, issuer, max_metadata_size)?;
128        provider.saml_config = Some(config);
129        identity_changed |= changed;
130    }
131    if let Some(updates) = body.get("oidcConfig") {
132        let current: Value = existing
133            .oidc_config
134            .as_deref()
135            .and_then(|raw| serde_json::from_str(raw).ok())
136            .filter(|value: &Value| !value.is_null())
137            .ok_or_else(|| {
138                api(
139                    400,
140                    "Cannot update OIDC config for a provider that doesn't have OIDC configured",
141                )
142            })?;
143        let mut merged = current.as_object().cloned().ok_or_else(|| {
144            api(
145                400,
146                "Cannot update OIDC config for a provider that doesn't have OIDC configured",
147            )
148        })?;
149        if let Some(fields) = updates.as_object() {
150            for (key, value) in fields {
151                _ = merged.insert(key.clone(), value.clone());
152            }
153        }
154        let issuer = body
155            .get("issuer")
156            .and_then(Value::as_str)
157            .or_else(|| current.get("issuer").and_then(Value::as_str))
158            .filter(|issuer| !issuer.is_empty())
159            .unwrap_or(&existing.issuer);
160        _ = merged.insert("issuer".into(), json!(issuer));
161        if merged.get("pkce").is_none_or(Value::is_null) {
162            _ = merged.insert("pkce".into(), json!(true));
163        }
164        let method = merged
165            .get("tokenEndpointAuthentication")
166            .and_then(Value::as_str)
167            .unwrap_or("client_secret_basic");
168        if method != "private_key_jwt"
169            && merged
170                .get("clientSecret")
171                .and_then(Value::as_str)
172                .is_none_or(str::is_empty)
173        {
174            return Err(api(
175                400,
176                "clientSecret is required when using client_secret_basic or client_secret_post authentication",
177            ));
178        }
179        if method == "private_key_jwt" && !private_key_configured {
180            return Err(api(
181                400,
182                "private_key_jwt authentication requires either a resolvePrivateKey callback or a privateKey in defaultSSO",
183            ));
184        }
185        let updated = Value::Object(merged);
186        identity_changed |= [
187            "authorizationEndpoint",
188            "clientId",
189            "discoveryEndpoint",
190            "jwksEndpoint",
191            "tokenEndpoint",
192            "userInfoEndpoint",
193        ]
194        .iter()
195        .any(|field| current.get(field) != updated.get(field));
196        provider.oidc_config = Some(serde_json::to_string(&updated)?);
197    }
198    Ok(SsoProviderUpdate {
199        provider,
200        identity_changed,
201        additional_field_changes: serde_json::Map::new(),
202    })
203}
204pub(super) fn validate_update(input: Option<&JsValue>) -> AuthResult<JsValue> {
205    use super::super::authentication_helpers::{JsonField, JsonFieldKind};
206    let mut output = super::super::endpoint::validate_fields(
207        input,
208        "body",
209        &[
210            JsonField::string("issuer", false),
211            JsonField::string("domain", false),
212            JsonField {
213                name: "oidcConfig",
214                kind: JsonFieldKind::Record,
215                required: false,
216            },
217            JsonField {
218                name: "samlConfig",
219                kind: JsonFieldKind::Record,
220                required: false,
221            },
222            JsonField::string("providerId", true),
223        ],
224    )?;
225    if output
226        .get("issuer")
227        .and_then(JsValue::as_str)
228        .is_some_and(|issuer| url::Url::parse(issuer).is_err())
229    {
230        return Err(super::super::endpoint::validation(
231            "[body.issuer] Invalid URL",
232        ));
233    }
234    if let Some(config) = output.get("oidcConfig") {
235        let original_mapping = config.get("mapping");
236        let config = super::super::endpoint::validate_fields(
237            Some(config),
238            "body.oidcConfig",
239            &[
240                JsonField::string("clientId", false),
241                JsonField::string("clientSecret", false),
242                JsonField::string("authorizationEndpoint", false),
243                JsonField::string("tokenEndpoint", false),
244                JsonField::string("userInfoEndpoint", false),
245                JsonField {
246                    name: "tokenEndpointAuthentication",
247                    kind: JsonFieldKind::OneOf(&[
248                        "client_secret_post",
249                        "client_secret_basic",
250                        "private_key_jwt",
251                    ]),
252                    required: false,
253                },
254                JsonField::string("privateKeyId", false),
255                JsonField::string("privateKeyAlgorithm", false),
256                JsonField::string("jwksEndpoint", false),
257                JsonField::string("discoveryEndpoint", false),
258                JsonField {
259                    name: "skipDiscovery",
260                    kind: JsonFieldKind::Boolean,
261                    required: false,
262                },
263                JsonField {
264                    name: "pkce",
265                    kind: JsonFieldKind::Boolean,
266                    required: false,
267                },
268                JsonField {
269                    name: "overrideUserInfo",
270                    kind: JsonFieldKind::Boolean,
271                    required: false,
272                },
273            ],
274        )?;
275        for field in super::discovery::ENDPOINT_FIELDS {
276            if let Some(value) = config.get(field).and_then(JsValue::as_str)
277                && url::Url::parse(value).is_err()
278            {
279                return Err(super::super::endpoint::validation(format!(
280                    "[body.oidcConfig.{field}] Invalid URL"
281                )));
282            }
283        }
284        let mut normalized = config
285            .as_object()
286            .cloned()
287            .ok_or_else(|| super::super::endpoint::validation("Invalid OIDC config"))?;
288        if let Some(mapping) = original_mapping {
289            _ = normalized.insert("mapping".into(), super::mapping_schema::oidc(mapping)?);
290        }
291        if let Some(scopes) = output
292            .get("oidcConfig")
293            .and_then(|config| config.get("scopes"))
294        {
295            super::oidc_scopes::validate(scopes)?;
296            _ = normalized.insert("scopes".into(), scopes.clone());
297        }
298        if let JsValue::Object(object) = &mut output {
299            _ = object.insert("oidcConfig".into(), JsValue::Object(normalized));
300        }
301    }
302    if let Some(config) = output.get("samlConfig") {
303        let config = super::registration_saml::validate_update(config)?;
304        if let JsValue::Object(object) = &mut output {
305            _ = object.insert("samlConfig".into(), config);
306        }
307    }
308    Ok(output)
309}