bucketwarden-auth 0.1.0

BucketWarden local identity, access key, and session credential store.
Documentation
use serde::{Deserialize, Serialize};

pub const ROLE_ASSUMPTION_RUNTIME_FEATURES: &[&str] = &[
    "AssumeRole",
    "AssumeRoleWithWebIdentity",
    "AssumeRoleWithLDAP",
    "Custom identity",
    "Token duration",
    "Policy scoping",
];

pub const ROLE_ASSUMPTION_ADMIN_SURFACES: &[&str] = &[
    "AuthStore::assume_role",
    "AuthStore::assume_role_with_web_identity",
    "AuthStore::assume_role_with_ldap",
    "AuthStore::put_custom_identity",
    "AuthStore::assume_role_with_custom_identity",
];

pub const ROLE_ASSUMPTION_SECURITY_CONTROLS: &[&str] = &[
    "known enabled principal enforcement",
    "ManageCredentials role gate for direct AssumeRole",
    "provider-kind gate for LDAP AssumeRole",
    "custom identity shared-secret verification",
    "scoped session issuance",
    "bounded token duration",
];

pub const ROLE_ASSUMPTION_OBSERVABILITY_FIELDS: &[&str] = &[
    "principal_id",
    "tenant_id",
    "role_name",
    "role_resource",
    "provider_id",
    "access_key_id",
    "scope",
    "expires_at_epoch_seconds",
];

pub const ROLE_ASSUMPTION_FAILURE_MODES: &[&str] = &[
    "UnknownPrincipal",
    "DisabledPrincipal",
    "RoleAssumptionDenied",
    "UnknownIdentityProvider",
    "UnsupportedIdentityProviderKind",
    "UnknownCustomIdentity",
    "DisabledCustomIdentity",
    "InvalidCustomIdentitySecret",
    "ExpiredCredential",
];

pub const ROLE_ASSUMPTION_VALIDATION_TESTS: &[&str] = &[
    "crates/bucketwarden-auth/tests/sts_scoped_sessions.rs",
    "crates/bucketwarden-auth/tests/external_identity_providers.rs",
    "crates/bucketwarden-auth/tests/role_assumption_support_contract.rs",
];

pub const ROLE_ASSUMPTION_CAVEATS: &[&str] = &[
    "Role authorization currently gates direct AssumeRole through BucketWarden operator roles rather than an AWS IAM trust-policy document.",
    "LDAP role assumption uses deterministic directory fixtures; production LDAP bind transport is a deployment integration.",
    "Custom identity assertions are local shared-secret records until external custom identity plugins are added.",
];

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct RoleAssumptionSupportReport {
    pub native_support_state: Vec<&'static str>,
    pub semantic_parity: &'static str,
    pub configuration_admin_surface: Vec<&'static str>,
    pub security_governance_impact: Vec<&'static str>,
    pub observability_evidence_fields: Vec<&'static str>,
    pub failure_modes: Vec<&'static str>,
    pub validation_test_coverage: Vec<&'static str>,
    pub product_specific_caveats: Vec<&'static str>,
}

impl RoleAssumptionSupportReport {
    pub fn current() -> Self {
        Self {
            native_support_state: ROLE_ASSUMPTION_RUNTIME_FEATURES.to_vec(),
            semantic_parity: "Role assumption issues scoped, expiring session credentials only after principal, provider, role, and identity assertions pass.",
            configuration_admin_surface: ROLE_ASSUMPTION_ADMIN_SURFACES.to_vec(),
            security_governance_impact: ROLE_ASSUMPTION_SECURITY_CONTROLS.to_vec(),
            observability_evidence_fields: ROLE_ASSUMPTION_OBSERVABILITY_FIELDS.to_vec(),
            failure_modes: ROLE_ASSUMPTION_FAILURE_MODES.to_vec(),
            validation_test_coverage: ROLE_ASSUMPTION_VALIDATION_TESTS.to_vec(),
            product_specific_caveats: ROLE_ASSUMPTION_CAVEATS.to_vec(),
        }
    }
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct CredentialScope {
    pub allowed_actions: Vec<String>,
    pub resource_prefixes: Vec<String>,
}

impl CredentialScope {
    pub fn new(allowed_actions: Vec<String>, resource_prefixes: Vec<String>) -> Self {
        Self {
            allowed_actions,
            resource_prefixes,
        }
    }

    pub fn permits(&self, action: &str, resource: &str) -> bool {
        self.action_permits(action) && self.resource_permits(resource)
    }

    fn action_permits(&self, action: &str) -> bool {
        self.allowed_actions
            .iter()
            .any(|allowed| allowed == "*" || allowed == action || wildcard_match(allowed, action))
    }

    fn resource_permits(&self, resource: &str) -> bool {
        self.resource_prefixes.iter().any(|prefix| {
            prefix == "*"
                || resource == prefix
                || resource.starts_with(prefix.trim_end_matches('*'))
        })
    }
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct AssumeRoleRequest {
    pub principal_id: String,
    pub role_name: String,
    pub role_resource: String,
    pub duration_seconds: u64,
    pub scope: CredentialScope,
    pub access_key_id: String,
    pub secret_access_key: String,
    pub session_token: String,
}

impl AssumeRoleRequest {
    pub fn new(
        principal_id: impl Into<String>,
        role_name: impl Into<String>,
        role_resource: impl Into<String>,
        duration_seconds: u64,
        scope: CredentialScope,
        access_key_id: impl Into<String>,
        secret_access_key: impl Into<String>,
        session_token: impl Into<String>,
    ) -> Self {
        Self {
            principal_id: principal_id.into(),
            role_name: role_name.into(),
            role_resource: role_resource.into(),
            duration_seconds,
            scope,
            access_key_id: access_key_id.into(),
            secret_access_key: secret_access_key.into(),
            session_token: session_token.into(),
        }
    }
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct AssumeRoleWithWebIdentityRequest {
    pub provider_id: String,
    pub web_identity_token: String,
    pub duration_seconds: u64,
    pub scope: CredentialScope,
    pub access_key_id: String,
    pub secret_access_key: String,
    pub session_token: String,
}

impl AssumeRoleWithWebIdentityRequest {
    pub fn new(
        provider_id: impl Into<String>,
        web_identity_token: impl Into<String>,
        duration_seconds: u64,
        scope: CredentialScope,
        access_key_id: impl Into<String>,
        secret_access_key: impl Into<String>,
        session_token: impl Into<String>,
    ) -> Self {
        Self {
            provider_id: provider_id.into(),
            web_identity_token: web_identity_token.into(),
            duration_seconds,
            scope,
            access_key_id: access_key_id.into(),
            secret_access_key: secret_access_key.into(),
            session_token: session_token.into(),
        }
    }
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct AssumeRoleWithCustomIdentityRequest {
    pub principal_id: String,
    pub shared_secret: String,
    pub duration_seconds: u64,
    pub scope: CredentialScope,
    pub access_key_id: String,
    pub secret_access_key: String,
    pub session_token: String,
}

impl AssumeRoleWithCustomIdentityRequest {
    pub fn new(
        principal_id: impl Into<String>,
        shared_secret: impl Into<String>,
        duration_seconds: u64,
        scope: CredentialScope,
        access_key_id: impl Into<String>,
        secret_access_key: impl Into<String>,
        session_token: impl Into<String>,
    ) -> Self {
        Self {
            principal_id: principal_id.into(),
            shared_secret: shared_secret.into(),
            duration_seconds,
            scope,
            access_key_id: access_key_id.into(),
            secret_access_key: secret_access_key.into(),
            session_token: session_token.into(),
        }
    }
}

fn wildcard_match(pattern: &str, value: &str) -> bool {
    let Some(prefix) = pattern.strip_suffix('*') else {
        return false;
    };
    value.starts_with(prefix)
}