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.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}