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.authentication_boundary_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    let mut trust_anchors_changed = false;
111    if let Some(issuer) = body.get("issuer").and_then(Value::as_str) {
112        identity_changed = issuer != existing.issuer;
113        provider.issuer = issuer.into();
114    }
115    if let Some(domain) = body.get("domain").and_then(Value::as_str) {
116        if domain != existing.domain {
117            provider.domain_verified = Some(false);
118        }
119        provider.domain = domain.into();
120    }
121    if let Some(updates) = body.get("samlConfig") {
122        super::signing_algorithms::validate(updates, signing_policy)?;
123        let issuer = body
124            .get("issuer")
125            .and_then(Value::as_str)
126            .filter(|issuer| !issuer.is_empty());
127        let prepared =
128            super::provider_update_saml::prepare(existing, updates, issuer, max_metadata_size)?;
129        provider.saml_config = Some(prepared.config);
130        identity_changed |= prepared.identity_changed;
131        trust_anchors_changed = prepared.trust_anchors_changed;
132    }
133    if let Some(updates) = body.get("oidcConfig") {
134        let current: Value = existing
135            .oidc_config
136            .as_deref()
137            .and_then(|raw| serde_json::from_str(raw).ok())
138            .filter(|value: &Value| !value.is_null())
139            .ok_or_else(|| {
140                api(
141                    400,
142                    "Cannot update OIDC config for a provider that doesn't have OIDC configured",
143                )
144            })?;
145        let mut merged = current.as_object().cloned().ok_or_else(|| {
146            api(
147                400,
148                "Cannot update OIDC config for a provider that doesn't have OIDC configured",
149            )
150        })?;
151        if let Some(fields) = updates.as_object() {
152            for (key, value) in fields {
153                _ = merged.insert(key.clone(), value.clone());
154            }
155        }
156        let issuer = body
157            .get("issuer")
158            .and_then(Value::as_str)
159            .or_else(|| current.get("issuer").and_then(Value::as_str))
160            .filter(|issuer| !issuer.is_empty())
161            .unwrap_or(&existing.issuer);
162        _ = merged.insert("issuer".into(), json!(issuer));
163        if merged.get("pkce").is_none_or(Value::is_null) {
164            _ = merged.insert("pkce".into(), json!(true));
165        }
166        let method = merged
167            .get("tokenEndpointAuthentication")
168            .and_then(Value::as_str)
169            .unwrap_or("client_secret_basic");
170        if method != "private_key_jwt"
171            && merged
172                .get("clientSecret")
173                .and_then(Value::as_str)
174                .is_none_or(str::is_empty)
175        {
176            return Err(api(
177                400,
178                "clientSecret is required when using client_secret_basic or client_secret_post authentication",
179            ));
180        }
181        if method == "private_key_jwt" && !private_key_configured {
182            return Err(api(
183                400,
184                "private_key_jwt authentication requires either a resolvePrivateKey callback or a privateKey in defaultSSO",
185            ));
186        }
187        let updated = Value::Object(merged);
188        identity_changed |= [
189            "authorizationEndpoint",
190            "clientId",
191            "discoveryEndpoint",
192            "jwksEndpoint",
193            "tokenEndpoint",
194            "userInfoEndpoint",
195        ]
196        .iter()
197        .any(|field| current.get(field) != updated.get(field));
198        provider.oidc_config = Some(serde_json::to_string(&updated)?);
199    }
200    Ok(SsoProviderUpdate {
201        provider,
202        identity_changed,
203        authentication_boundary_changed: identity_changed || trust_anchors_changed,
204        additional_field_changes: serde_json::Map::new(),
205    })
206}
207pub(super) fn validate_update(input: Option<&JsValue>) -> AuthResult<JsValue> {
208    use super::super::authentication_helpers::{JsonField, JsonFieldKind};
209    let mut output = super::super::endpoint::validate_fields(
210        input,
211        "body",
212        &[
213            JsonField::string("issuer", false),
214            JsonField::string("domain", false),
215            JsonField {
216                name: "oidcConfig",
217                kind: JsonFieldKind::Record,
218                required: false,
219            },
220            JsonField {
221                name: "samlConfig",
222                kind: JsonFieldKind::Record,
223                required: false,
224            },
225            JsonField::string("providerId", true),
226        ],
227    )?;
228    if output
229        .get("issuer")
230        .and_then(JsValue::as_str)
231        .is_some_and(|issuer| url::Url::parse(issuer).is_err())
232    {
233        return Err(super::super::endpoint::validation(
234            "[body.issuer] Invalid URL",
235        ));
236    }
237    if let Some(config) = output.get("oidcConfig") {
238        let original_mapping = config.get("mapping");
239        let config = super::super::endpoint::validate_fields(
240            Some(config),
241            "body.oidcConfig",
242            &[
243                JsonField::string("clientId", false),
244                JsonField::string("clientSecret", false),
245                JsonField::string("authorizationEndpoint", false),
246                JsonField::string("tokenEndpoint", false),
247                JsonField::string("userInfoEndpoint", false),
248                JsonField {
249                    name: "tokenEndpointAuthentication",
250                    kind: JsonFieldKind::OneOf(&[
251                        "client_secret_post",
252                        "client_secret_basic",
253                        "private_key_jwt",
254                    ]),
255                    required: false,
256                },
257                JsonField::string("privateKeyId", false),
258                JsonField::string("privateKeyAlgorithm", false),
259                JsonField::string("jwksEndpoint", false),
260                JsonField::string("discoveryEndpoint", false),
261                JsonField {
262                    name: "skipDiscovery",
263                    kind: JsonFieldKind::Boolean,
264                    required: false,
265                },
266                JsonField {
267                    name: "pkce",
268                    kind: JsonFieldKind::Boolean,
269                    required: false,
270                },
271                JsonField {
272                    name: "overrideUserInfo",
273                    kind: JsonFieldKind::Boolean,
274                    required: false,
275                },
276            ],
277        )?;
278        for field in super::discovery::ENDPOINT_FIELDS {
279            if let Some(value) = config.get(field).and_then(JsValue::as_str)
280                && url::Url::parse(value).is_err()
281            {
282                return Err(super::super::endpoint::validation(format!(
283                    "[body.oidcConfig.{field}] Invalid URL"
284                )));
285            }
286        }
287        let mut normalized = config
288            .as_object()
289            .cloned()
290            .ok_or_else(|| super::super::endpoint::validation("Invalid OIDC config"))?;
291        if let Some(mapping) = original_mapping {
292            _ = normalized.insert("mapping".into(), super::mapping_schema::oidc(mapping)?);
293        }
294        if let Some(scopes) = output
295            .get("oidcConfig")
296            .and_then(|config| config.get("scopes"))
297        {
298            super::oidc_scopes::validate(scopes)?;
299            _ = normalized.insert("scopes".into(), scopes.clone());
300        }
301        if let JsValue::Object(object) = &mut output {
302            _ = object.insert("oidcConfig".into(), JsValue::Object(normalized));
303        }
304    }
305    if let Some(config) = output.get("samlConfig") {
306        let config = super::registration_saml::validate_update(config)?;
307        if let JsValue::Object(object) = &mut output {
308            _ = object.insert("samlConfig".into(), config);
309        }
310    }
311    Ok(output)
312}