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