1use 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}