alibi-plugins 0.9.0

Built-in authentication plugins and API metadata for Alibi
Documentation
//! Authorized provider configuration without application credentials.
use super::{
    AuthContext, AuthError, AuthResult, AuthSchema, AuthUser, EndpointResponse, JsValue,
    ServerEndpoint, SessionRequest, SsoPlugin, SsoProvider, Value, api, json,
};
impl SsoPlugin {
    pub fn get_sso_provider_endpoint(provider_id: &str) -> AuthResult<ServerEndpoint<Value>> {
        ServerEndpoint::new("sso", "getSSOProvider").with_query(&json!({"providerId":provider_id}))
    }
    pub(super) async fn read<S: AuthSchema>(
        &self,
        input: &JsValue,
        request: &impl SessionRequest,
        ctx: &AuthContext<S>,
    ) -> AuthResult<EndpointResponse> {
        let provider = self.authorized_provider(input, request, ctx).await?;
        EndpointResponse::json(&sanitize(&provider, ctx)?)
    }
    pub(super) async fn authorized_provider<S: AuthSchema>(
        &self,
        input: &JsValue,
        request: &impl SessionRequest,
        ctx: &AuthContext<S>,
    ) -> AuthResult<SsoProvider> {
        let (user, _) = super::super::helpers::ordinary_session(request, ctx).await?;
        let provider_id = input
            .get("providerId")
            .and_then(JsValue::as_str)
            .ok_or_else(|| super::super::endpoint::validation("Invalid providerId"))?;
        self.authorize_provider_id(provider_id, user.id().as_ref(), ctx)
            .await
    }
    pub(super) async fn authorize_provider_id<S: AuthSchema>(
        &self,
        provider_id: &str,
        user_id: &str,
        ctx: &AuthContext<S>,
    ) -> AuthResult<SsoProvider> {
        let provider = ctx
            .database
            .get_sso_provider(provider_id)
            .await?
            .ok_or_else(|| api(404, "Provider not found"))?;
        let allowed = if let Some(organization_id) = provider
            .organization_id
            .as_deref()
            .filter(|id| !id.is_empty())
        {
            if ctx
                .get_metadata(super::super::organization::METADATA_ENABLED)
                .is_some()
            {
                ctx.database
                    .get_member(organization_id, user_id)
                    .await?
                    .is_some_and(|member| {
                        member
                            .role
                            .split(',')
                            .any(|role| matches!(role.trim(), "owner" | "admin"))
                    })
            } else {
                provider.user_id.as_deref() == Some(user_id)
            }
        } else {
            provider.user_id.as_deref() == Some(user_id)
        };
        if !allowed {
            return Err(api(403, "You don't have access to this provider"));
        }
        Ok(provider)
    }
}
pub(super) fn sanitize<S: AuthSchema>(
    provider: &SsoProvider,
    ctx: &AuthContext<S>,
) -> AuthResult<Value> {
    let saml = provider
        .saml_config
        .as_deref()
        .and_then(|raw| serde_json::from_str::<Value>(raw).ok())
        .filter(|value| !value.is_null());
    let base = format!(
        "{}{}",
        ctx.config.base_url.trim_end_matches('/'),
        ctx.config.base_path.trim_end_matches('/')
    );
    let mut output = json!({"providerId":provider.provider_id,"type":if saml.is_some(){"saml"}else{"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))});
    if let Some(config) = saml {
        let mut safe = serde_json::Map::new();
        for field in [
            "entryPoint",
            "callbackUrl",
            "idpInitiatedCallbackUrl",
            "audience",
            "wantAssertionsSigned",
            "authnRequestsSigned",
            "identifierFormat",
            "signatureAlgorithm",
            "digestAlgorithm",
        ] {
            if let Some(value) = config.get(field) {
                _ = safe.insert(field.into(), value.clone());
            }
        }
        if let Some(certs) = config
            .get("idpMetadata")
            .and_then(|metadata| metadata.get("cert"))
            .filter(|cert| !cert.is_null())
            .or_else(|| config.get("cert"))
        {
            let certs = certs
                .as_array()
                .map_or_else(|| vec![certs], |certs| certs.iter().collect());
            let details = certs
                .into_iter()
                .map(|cert| {
                    cert.as_str().map_or_else(
                        || json!({"error":"Failed to parse certificate"}),
                        certificate_details,
                    )
                })
                .collect::<Vec<_>>();
            _ = safe.insert("certificate".into(), json!(details));
        }
        let object = output
            .as_object_mut()
            .ok_or_else(|| AuthError::internal("SSO provider serialization failed"))?;
        _ = object.insert("samlConfig".into(), Value::Object(safe));
    }
    if let Some(config) = provider
        .oidc_config
        .as_deref()
        .and_then(|raw| serde_json::from_str::<Value>(raw).ok())
        .filter(|value| !value.is_null())
    {
        let mut safe = serde_json::Map::new();
        for field in [
            "discoveryEndpoint",
            "pkce",
            "authorizationEndpoint",
            "tokenEndpoint",
            "userInfoEndpoint",
            "jwksEndpoint",
            "scopes",
            "tokenEndpointAuthentication",
        ] {
            if let Some(value) = config.get(field) {
                _ = safe.insert(field.into(), value.clone());
            }
        }
        let client_id = config
            .get("clientId")
            .and_then(Value::as_str)
            .ok_or_else(|| AuthError::internal("Stored OIDC configuration has no clientId"))?;
        let units: Vec<u16> = client_id.encode_utf16().collect();
        let last = units
            .get(units.len().saturating_sub(4)..)
            .unwrap_or_default();
        let masked = if units.len() <= 4 {
            "****".into()
        } else {
            format!("****{}", String::from_utf16_lossy(last))
        };
        _ = safe.insert("clientIdLastFour".into(), json!(masked));
        let object = output
            .as_object_mut()
            .ok_or_else(|| AuthError::internal("SSO provider serialization failed"))?;
        _ = object.insert("oidcConfig".into(), Value::Object(safe));
    }
    if let Some(object) = output.as_object_mut() {
        for (name, field) in &ctx.config.advanced.database.sso_provider_fields.0 {
            if field.returned
                && let Some(value) = provider.additional_fields.get(name)
            {
                _ = object.insert(name.clone(), value.clone());
            }
        }
    }
    Ok(output)
}
fn component(value: &str) -> String {
    urlencoding::encode(value)
        .replace("%21", "!")
        .replace("%27", "'")
        .replace("%28", "(")
        .replace("%29", ")")
        .replace("%2A", "*")
}
pub(super) fn validate_query(value: Option<&JsValue>) -> AuthResult<JsValue> {
    super::super::endpoint::validate_fields(
        value,
        "query",
        &[super::super::authentication_helpers::JsonField::string(
            "providerId",
            true,
        )],
    )
}

fn certificate_details(pem: &str) -> Value {
    use base64::Engine as _;
    use sha2::Digest as _;
    let parse = || -> Option<Value> {
        let encoded = if pem.contains("-----BEGIN") {
            pem.split_once("-----BEGIN CERTIFICATE-----")?
                .1
                .split_once("-----END CERTIFICATE-----")?
                .0
        } else {
            pem
        };
        let encoded: String = encoded.chars().filter(|ch| !ch.is_whitespace()).collect();
        let der = base64::engine::general_purpose::STANDARD
            .decode(encoded)
            .ok()?;
        let (_, certificate) = x509_parser::parse_x509_certificate(&der).ok()?;
        let fingerprint = sha2::Sha256::digest(&der)
            .iter()
            .map(|byte| format!("{byte:02X}"))
            .collect::<Vec<_>>()
            .join(":");
        let date = |timestamp| {
            chrono::DateTime::from_timestamp(timestamp, 0)
                .map(|time| time.format("%b %e %H:%M:%S %Y GMT").to_string())
        };
        let algorithm = match certificate
            .public_key()
            .algorithm
            .algorithm
            .to_id_string()
            .as_str()
        {
            "1.2.840.113549.1.1.1" => "RSA",
            "1.2.840.113549.1.1.10" => "RSA-PSS",
            "1.2.840.10045.2.1" => "EC",
            "1.2.840.10040.4.1" => "DSA",
            "1.3.101.112" => "ED25519",
            "1.3.101.113" => "ED448",
            "1.3.101.110" => "X25519",
            "1.3.101.111" => "X448",
            "1.2.840.113549.1.3.1" | "1.2.840.10046.2.1" => "DH",
            _ => "UNKNOWN",
        };
        Some(json!({"fingerprintSha256":fingerprint,
            "notBefore":date(certificate.validity().not_before.timestamp())?,
            "notAfter":date(certificate.validity().not_after.timestamp())?,
            "publicKeyAlgorithm":algorithm}))
    };
    parse().unwrap_or_else(|| json!({"error":"Failed to parse certificate"}))
}