wyrd_runtime/
permission_check.rs1use crate::permission::Permission;
4use crate::principal::{Principal, PrincipalId};
5
6pub trait PermissionCheck: Send + Sync + std::fmt::Debug {
12 fn check(&self, principal: &Principal, permission: &Permission) -> PermissionVerdict;
14}
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum PermissionVerdict {
19 Allow,
21 Deny {
23 reason: PermissionDenyReason,
25 },
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum PermissionDenyReason {
34 Rbac {
37 required: Box<Permission>,
42 principal: PrincipalId,
44 },
45}
46
47impl PermissionVerdict {
48 pub fn into_result(self) -> Result<(), PermissionDenyReason> {
50 match self {
51 Self::Allow => Ok(()),
52 Self::Deny { reason } => Err(reason),
53 }
54 }
55}
56
57#[derive(Debug, Default, Clone, Copy)]
62pub struct RbacCheck;
63
64impl PermissionCheck for RbacCheck {
65 fn check(&self, principal: &Principal, permission: &Permission) -> PermissionVerdict {
66 if principal.effective_permissions.contains(permission) {
67 PermissionVerdict::Allow
68 } else {
69 PermissionVerdict::Deny {
70 reason: PermissionDenyReason::Rbac {
71 required: Box::new(permission.clone()),
72 principal: principal.id,
73 },
74 }
75 }
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::{PermissionCheck, PermissionDenyReason, PermissionVerdict, RbacCheck};
82 use crate::permission::{Permission, PermissionSet};
83 use crate::principal::{Principal, PrincipalId, PrincipalKind, RoleRef};
84
85 fn principal_with_permissions(permissions: impl IntoIterator<Item = Permission>) -> Principal {
86 Principal::new(
87 PrincipalId::new(uuid::Uuid::now_v7()),
88 PrincipalKind::User,
89 wyrd_spec::DataTenantId::new_v7(),
90 vec![RoleRef::new("editor").expect("static role is valid")],
91 PermissionSet::from_iter(permissions),
92 )
93 }
94
95 #[test]
96 fn rbac_allows_match() {
97 let principal = principal_with_permissions([Permission::card_write()]);
98
99 let verdict = RbacCheck.check(&principal, &Permission::card_write());
100
101 assert_eq!(verdict, PermissionVerdict::Allow);
102 }
103
104 #[test]
105 fn rbac_denies_missing() {
106 let principal = principal_with_permissions([]);
107
108 let verdict = RbacCheck.check(&principal, &Permission::card_write());
109
110 assert_eq!(
111 verdict,
112 PermissionVerdict::Deny {
113 reason: PermissionDenyReason::Rbac {
114 required: Box::new(Permission::card_write()),
115 principal: principal.id,
116 },
117 }
118 );
119 }
120
121 #[test]
127 fn rbac_resolves_through_wildcard() {
128 let principal = principal_with_permissions([Permission::wildcard()]);
129
130 assert_eq!(
131 RbacCheck.check(&principal, &Permission::card_write()),
132 PermissionVerdict::Allow
133 );
134 assert_eq!(
135 RbacCheck.check(&principal, &Permission::bifrost_record_write()),
136 PermissionVerdict::Allow
137 );
138 }
139
140 #[test]
141 fn verdict_into_result_round_trips() {
142 assert_eq!(PermissionVerdict::Allow.into_result(), Ok(()));
143
144 let reason = PermissionDenyReason::Rbac {
145 required: Box::new(Permission::card_write()),
146 principal: PrincipalId::new(uuid::Uuid::now_v7()),
147 };
148 let verdict = PermissionVerdict::Deny {
149 reason: reason.clone(),
150 };
151
152 assert_eq!(verdict.into_result(), Err(reason));
153 }
154
155 #[test]
156 fn deny_reason_carries_required_and_principal() {
157 let reason = PermissionDenyReason::Rbac {
158 required: Box::new(Permission::card_write()),
159 principal: PrincipalId::new(uuid::Uuid::now_v7()),
160 };
161
162 let debug = format!("{reason:?}");
163
164 assert!(debug.contains("required:"));
165 assert!(debug.contains("principal:"));
166 }
167}