Skip to main content

alibi_plugins/sso/
provider_read.rs

1//! Authorized provider configuration without application credentials.
2use super::{
3    AuthContext, AuthError, AuthResult, AuthSchema, AuthUser, EndpointResponse, JsValue,
4    ServerEndpoint, SessionRequest, SsoPlugin, SsoProvider, Value, api, json,
5};
6impl SsoPlugin {
7    pub fn get_sso_provider_endpoint(provider_id: &str) -> AuthResult<ServerEndpoint<Value>> {
8        ServerEndpoint::new("sso", "getSSOProvider").with_query(&json!({"providerId":provider_id}))
9    }
10    pub(super) async fn read<S: AuthSchema>(
11        &self,
12        input: &JsValue,
13        request: &impl SessionRequest,
14        ctx: &AuthContext<S>,
15    ) -> AuthResult<EndpointResponse> {
16        let (user, _) = super::super::helpers::ordinary_session(request, ctx).await?;
17        let provider_id = input
18            .get("providerId")
19            .and_then(JsValue::as_str)
20            .ok_or_else(|| super::super::endpoint::validation("Invalid providerId"))?;
21        let provider = ctx
22            .database
23            .get_sso_provider(provider_id)
24            .await?
25            .ok_or_else(|| api(404, "Provider not found"))?;
26        let allowed = if let Some(organization_id) = provider
27            .organization_id
28            .as_deref()
29            .filter(|id| !id.is_empty())
30        {
31            if ctx
32                .get_metadata(super::super::organization::METADATA_ENABLED)
33                .is_some()
34            {
35                ctx.database
36                    .get_member(organization_id, user.id().as_ref())
37                    .await?
38                    .is_some_and(|member| {
39                        member
40                            .role
41                            .split(',')
42                            .any(|role| matches!(role.trim(), "owner" | "admin"))
43                    })
44            } else {
45                provider.user_id.as_deref() == Some(user.id().as_ref())
46            }
47        } else {
48            provider.user_id.as_deref() == Some(user.id().as_ref())
49        };
50        if !allowed {
51            return Err(api(403, "You don't have access to this provider"));
52        }
53        EndpointResponse::json(&sanitize(&provider, ctx)?)
54    }
55}
56pub(super) fn sanitize<S: AuthSchema>(
57    provider: &SsoProvider,
58    ctx: &AuthContext<S>,
59) -> AuthResult<Value> {
60    let saml = provider
61        .saml_config
62        .as_deref()
63        .and_then(|raw| serde_json::from_str::<Value>(raw).ok())
64        .filter(|value| !value.is_null());
65    if saml.is_some() {
66        return Err(AuthError::not_implemented(
67            "SAML provider sanitization is not implemented",
68        ));
69    }
70    let base = format!(
71        "{}{}",
72        ctx.config.base_url.trim_end_matches('/'),
73        ctx.config.base_path.trim_end_matches('/')
74    );
75    let mut output = json!({"providerId":provider.provider_id,"type":"oidc","issuer":provider.issuer,"domain":provider.domain,"organizationId":provider.organization_id.as_deref().filter(|id|!id.is_empty()),"domainVerified":provider.domain_verified.unwrap_or(false),"spMetadataUrl":format!("{base}/sso/saml2/sp/metadata?providerId={}",component(&provider.provider_id))});
76    if let Some(config) = provider
77        .oidc_config
78        .as_deref()
79        .and_then(|raw| serde_json::from_str::<Value>(raw).ok())
80        .filter(|value| !value.is_null())
81    {
82        let mut safe = serde_json::Map::new();
83        for field in [
84            "discoveryEndpoint",
85            "pkce",
86            "authorizationEndpoint",
87            "tokenEndpoint",
88            "userInfoEndpoint",
89            "jwksEndpoint",
90            "scopes",
91            "tokenEndpointAuthentication",
92        ] {
93            if let Some(value) = config.get(field) {
94                _ = safe.insert(field.into(), value.clone());
95            }
96        }
97        let client_id = config
98            .get("clientId")
99            .and_then(Value::as_str)
100            .ok_or_else(|| AuthError::internal("Stored OIDC configuration has no clientId"))?;
101        let units: Vec<u16> = client_id.encode_utf16().collect();
102        let last = units
103            .get(units.len().saturating_sub(4)..)
104            .unwrap_or_default();
105        let masked = if units.len() <= 4 {
106            "****".into()
107        } else {
108            format!("****{}", String::from_utf16_lossy(last))
109        };
110        _ = safe.insert("clientIdLastFour".into(), json!(masked));
111        let object = output
112            .as_object_mut()
113            .ok_or_else(|| AuthError::internal("SSO provider serialization failed"))?;
114        _ = object.insert("oidcConfig".into(), Value::Object(safe));
115    }
116    Ok(output)
117}
118fn component(value: &str) -> String {
119    urlencoding::encode(value)
120        .replace("%21", "!")
121        .replace("%27", "'")
122        .replace("%28", "(")
123        .replace("%29", ")")
124        .replace("%2A", "*")
125}
126pub(super) fn validate_query(value: Option<&JsValue>) -> AuthResult<JsValue> {
127    super::super::endpoint::validate_fields(
128        value,
129        "query",
130        &[super::super::authentication_helpers::JsonField::string(
131            "providerId",
132            true,
133        )],
134    )
135}