use crate::{
cdk::types::Principal,
dto::auth::DelegatedRoleGrant,
ids::{CanisterRole, cap},
};
use thiserror::Error as ThisError;
mod root_provisioning;
pub use root_provisioning::{
RootDelegatedRoleGrantPolicy, RootDelegationAudiencePolicy,
RootDelegationProofPreparePolicyDecision, RootDelegationProofPreparePolicyInput,
RootIssuerPolicy, validate_root_delegation_proof_prepare_policy,
};
#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
pub enum AuthPolicyError {
#[error(
"delegated token prepare public issuance scope '{scope}' is not self-grantable for role {role}"
)]
PublicPrepareScopeNotSelfGrantable { role: CanisterRole, scope: String },
#[error("root issuer audience is not allowed for issuer {issuer_pid}")]
RootIssuerAudienceNotAllowed { issuer_pid: Principal },
#[error("root issuer certificate TTL must be greater than zero")]
RootIssuerCertTtlZero,
#[error(
"root issuer certificate TTL {cert_ttl_ns} exceeds max certificate TTL {max_cert_ttl_ns}"
)]
RootIssuerCertTtlExceedsMax {
cert_ttl_ns: u64,
max_cert_ttl_ns: u64,
},
#[error("root issuer {issuer_pid} is disabled")]
RootIssuerDisabled { issuer_pid: Principal },
#[error("root issuer grant scope '{scope}' is not allowed for role {role}")]
RootIssuerGrantNotAllowed { role: CanisterRole, scope: String },
#[error("root issuer policy is for {expected}, but request named issuer {found}")]
RootIssuerPolicyMismatch {
expected: Principal,
found: Principal,
},
#[error("root issuer refresh-after offset must be within the certificate TTL")]
RootIssuerRefreshAfterInvalid,
#[error("root issuer refresh-after timestamp overflows nanoseconds")]
RootIssuerRefreshAfterOverflow,
#[error("root issuer refresh ratio must be between 1 and 9999 basis points")]
RootIssuerRefreshRatioInvalid { refresh_after_ratio_bps: u16 },
#[error("root issuer is not registered")]
RootIssuerUnregistered,
#[error("delegated token prepare subject must match caller")]
SubjectCallerMismatch,
}
pub fn validate_public_delegated_token_prepare(
caller: Principal,
subject: Principal,
grants: &[DelegatedRoleGrant],
) -> Result<(), AuthPolicyError> {
if subject != caller {
return Err(AuthPolicyError::SubjectCallerMismatch);
}
for grant in grants {
for scope in &grant.scopes {
if !public_delegated_token_prepare_scope(scope) {
return Err(AuthPolicyError::PublicPrepareScopeNotSelfGrantable {
role: grant.target.clone(),
scope: scope.clone(),
});
}
}
}
Ok(())
}
#[must_use]
pub fn public_delegated_token_prepare_scope(scope: &str) -> bool {
scope == cap::SESSION || scope == cap::VERIFY
}
#[cfg(test)]
mod tests {
use super::*;
fn p(id: u8) -> Principal {
Principal::from_slice(&[id; 29])
}
fn grant(role: &str, scopes: &[&str]) -> DelegatedRoleGrant {
DelegatedRoleGrant {
target: CanisterRole::owned(role.to_string()),
scopes: scopes.iter().map(|scope| (*scope).to_string()).collect(),
}
}
#[test]
fn public_prepare_allows_login_scopes_for_subnet_wide_tokens() {
validate_public_delegated_token_prepare(
p(7),
p(7),
&[
grant("user_shard", &[cap::SESSION]),
grant("project_instance", &[cap::VERIFY]),
],
)
.expect("login scopes should be public-issuable");
}
#[test]
fn public_prepare_rejects_subject_mismatch() {
let err = validate_public_delegated_token_prepare(
p(7),
p(8),
&[grant("project_instance", &[cap::SESSION])],
)
.expect_err("subject must bind to caller");
assert_eq!(err, AuthPolicyError::SubjectCallerMismatch);
}
#[test]
fn public_prepare_rejects_privileged_or_custom_scopes() {
for denied in [cap::READ, cap::WRITE, cap::ADMIN, "toko.admin"] {
let err = validate_public_delegated_token_prepare(
p(7),
p(7),
&[grant("project_instance", &[denied])],
)
.expect_err("privileged scope must not be self-grantable");
assert_eq!(
err,
AuthPolicyError::PublicPrepareScopeNotSelfGrantable {
role: CanisterRole::owned("project_instance".to_string()),
scope: denied.to_string(),
}
);
}
}
}