use crate::permission::Permission;
use crate::principal::{Principal, PrincipalId};
pub trait PermissionCheck: Send + Sync + std::fmt::Debug {
fn check(&self, principal: &Principal, permission: &Permission) -> PermissionVerdict;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PermissionVerdict {
Allow,
Deny {
reason: PermissionDenyReason,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PermissionDenyReason {
Rbac {
required: Box<Permission>,
principal: PrincipalId,
},
}
impl PermissionVerdict {
pub fn into_result(self) -> Result<(), PermissionDenyReason> {
match self {
Self::Allow => Ok(()),
Self::Deny { reason } => Err(reason),
}
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct RbacCheck;
impl PermissionCheck for RbacCheck {
fn check(&self, principal: &Principal, permission: &Permission) -> PermissionVerdict {
if principal.effective_permissions.contains(permission) {
PermissionVerdict::Allow
} else {
PermissionVerdict::Deny {
reason: PermissionDenyReason::Rbac {
required: Box::new(permission.clone()),
principal: principal.id,
},
}
}
}
}
#[cfg(test)]
mod tests {
use super::{PermissionCheck, PermissionDenyReason, PermissionVerdict, RbacCheck};
use crate::permission::{Permission, PermissionSet};
use crate::principal::{Principal, PrincipalId, PrincipalKind, RoleRef};
fn principal_with_permissions(permissions: impl IntoIterator<Item = Permission>) -> Principal {
Principal {
id: PrincipalId::new(uuid::Uuid::now_v7()),
kind: PrincipalKind::User,
tenant_id: wyrd_spec::DataTenantId::new_v7(),
roles: vec![RoleRef::new("editor").expect("static role is valid")],
effective_permissions: PermissionSet::from_iter(permissions),
credential_id: None,
}
}
#[test]
fn rbac_allows_match() {
let principal = principal_with_permissions([Permission::card_write()]);
let verdict = RbacCheck.check(&principal, &Permission::card_write());
assert_eq!(verdict, PermissionVerdict::Allow);
}
#[test]
fn rbac_denies_missing() {
let principal = principal_with_permissions([]);
let verdict = RbacCheck.check(&principal, &Permission::card_write());
assert_eq!(
verdict,
PermissionVerdict::Deny {
reason: PermissionDenyReason::Rbac {
required: Box::new(Permission::card_write()),
principal: principal.id,
},
}
);
}
#[test]
fn rbac_resolves_through_wildcard() {
let principal = principal_with_permissions([Permission::wildcard()]);
assert_eq!(
RbacCheck.check(&principal, &Permission::card_write()),
PermissionVerdict::Allow
);
assert_eq!(
RbacCheck.check(&principal, &Permission::bifrost_record_write()),
PermissionVerdict::Allow
);
}
#[test]
fn verdict_into_result_round_trips() {
assert_eq!(PermissionVerdict::Allow.into_result(), Ok(()));
let reason = PermissionDenyReason::Rbac {
required: Box::new(Permission::card_write()),
principal: PrincipalId::new(uuid::Uuid::now_v7()),
};
let verdict = PermissionVerdict::Deny {
reason: reason.clone(),
};
assert_eq!(verdict.into_result(), Err(reason));
}
#[test]
fn deny_reason_carries_required_and_principal() {
let reason = PermissionDenyReason::Rbac {
required: Box::new(Permission::card_write()),
principal: PrincipalId::new(uuid::Uuid::now_v7()),
};
let debug = format!("{reason:?}");
assert!(debug.contains("required:"));
assert!(debug.contains("principal:"));
}
}