1use crate::abi::{CapabilityMask, InstanceId, Principal};
14use core::marker::PhantomData;
15
16use super::op::Op;
17
18pub(crate) type InvariantLifetime<'i> = PhantomData<fn(&'i ()) -> &'i ()>;
21
22mod seal {
23 pub trait Sealed {}
24}
25
26#[derive(Debug)]
28pub enum Unverified {}
29
30#[derive(Debug)]
32pub enum Authorized {}
33
34impl seal::Sealed for Unverified {}
35impl seal::Sealed for Authorized {}
36
37pub trait AuthState: seal::Sealed {}
39impl AuthState for Unverified {}
40impl AuthState for Authorized {}
41
42#[derive(Debug)]
48pub struct Effect<'i, S: AuthState> {
49 pub(crate) instance_id: InstanceId,
50 pub(crate) principal: Principal,
51 pub(crate) op: Op,
52 _state: PhantomData<S>,
53 _brand: InvariantLifetime<'i>,
54}
55
56impl<'i> Effect<'i, Unverified> {
57 pub(crate) fn new(instance_id: InstanceId, principal: Principal, op: Op) -> Self {
60 Self {
61 instance_id,
62 principal,
63 op,
64 _state: PhantomData,
65 _brand: PhantomData,
66 }
67 }
68}
69
70impl<'i, S: AuthState> Effect<'i, S> {
71 pub fn instance_id(&self) -> InstanceId {
74 self.instance_id
75 }
76}
77
78#[non_exhaustive]
80#[derive(Clone, Debug, PartialEq, Eq)]
81pub enum DenyReason {
82 CapabilityDenied,
84 InstanceMismatch,
86 OperationRestricted,
89 NotImplemented,
91}
92
93impl core::fmt::Display for DenyReason {
94 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
95 match self {
96 Self::CapabilityDenied => write!(f, "capability denied"),
97 Self::InstanceMismatch => write!(f, "instance mismatch"),
98 Self::OperationRestricted => write!(f, "operation restricted"),
99 Self::NotImplemented => write!(f, "authorize not implemented"),
100 }
101 }
102}
103
104impl std::error::Error for DenyReason {}
105
106pub(crate) fn effective_caps(
121 default_caps: CapabilityMask,
122 principal: &Principal,
123 ceiling: CapabilityMask,
124) -> CapabilityMask {
125 match principal {
126 Principal::System | Principal::External(_) => default_caps & ceiling,
127 Principal::Unauthenticated => CapabilityMask::empty(),
128 }
129}
130
131pub(crate) fn authorize<'i>(
142 caps: CapabilityMask,
143 effect: Effect<'i, Unverified>,
144) -> Result<Effect<'i, Authorized>, DenyReason> {
145 match &effect.principal {
146 Principal::Unauthenticated => return Err(DenyReason::CapabilityDenied),
147 Principal::System | Principal::External(_) => {
148 if !caps.contains(CapabilityMask::SYSTEM) && !match_op_cap(&effect.op, caps) {
149 return Err(DenyReason::CapabilityDenied);
150 }
151 }
152 }
153 Ok(Effect {
154 instance_id: effect.instance_id,
155 principal: effect.principal,
156 op: effect.op,
157 _state: PhantomData,
158 _brand: PhantomData,
159 })
160}
161
162fn match_op_cap(op: &Op, caps: CapabilityMask) -> bool {
165 match op {
166 Op::SpawnEntity { .. }
168 | Op::DespawnEntity { .. }
169 | Op::SetComponent { .. }
170 | Op::RemoveComponent { .. }
171 | Op::EmitEvent { .. } => true,
172 Op::ScheduleAction { .. } | Op::SendSignal { .. } => caps.contains(CapabilityMask::SYSTEM),
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use crate::abi::{EntityId, ExternalId, RouteId, Tick, TypeCode};
181 use bytes::Bytes;
182
183 fn inst() -> InstanceId {
184 InstanceId::new(1).unwrap()
185 }
186 fn ent() -> EntityId {
187 EntityId::new(1).unwrap()
188 }
189
190 #[test]
191 fn unverified_is_uninhabited() {
192 fn _proof(x: Unverified) -> ! {
193 match x {}
194 }
195 }
196
197 #[test]
198 fn authorized_is_uninhabited() {
199 fn _proof(x: Authorized) -> ! {
200 match x {}
201 }
202 }
203
204 #[test]
205 fn effect_carries_instance_id_and_op() {
206 let e: Effect<'_, Unverified> = Effect::new(
207 inst(),
208 Principal::System,
209 Op::SpawnEntity {
210 id: ent(),
211 owner: Principal::System,
212 },
213 );
214 assert_eq!(e.instance_id().get(), 1);
215 }
216
217 #[test]
218 fn auth_state_seal_blocks_external_impl() {
219 fn assert_authstate<T: AuthState>() {}
220 assert_authstate::<Unverified>();
221 assert_authstate::<Authorized>();
222 }
223
224 #[test]
225 fn deny_reason_display_and_error() {
226 assert_eq!(
227 format!("{}", DenyReason::CapabilityDenied),
228 "capability denied"
229 );
230 assert_eq!(
231 format!("{}", DenyReason::OperationRestricted),
232 "operation restricted"
233 );
234 fn assert_err<E: std::error::Error>() {}
235 assert_err::<DenyReason>();
236 }
237
238 fn spawn_op() -> Op {
241 Op::SpawnEntity {
242 id: ent(),
243 owner: Principal::System,
244 }
245 }
246 fn schedule_op() -> Op {
247 Op::ScheduleAction {
248 at: Tick(0),
249 actor: None,
250 action_type_code: TypeCode(0),
251 action_bytes: Bytes::new(),
252 }
253 }
254 fn signal_op() -> Op {
255 Op::SendSignal {
256 target: inst(),
257 route: RouteId(1),
258 payload: Bytes::new(),
259 }
260 }
261
262 #[test]
263 fn system_principal_gated_by_effective_caps_not_blanket_bypass() {
264 for op in [schedule_op(), signal_op()] {
268 let denied = authorize(CapabilityMask::empty(), Effect::new(inst(), Principal::System, op));
269 assert_eq!(
270 denied.unwrap_err(),
271 DenyReason::CapabilityDenied,
272 "System with empty caps must NOT pass a SYSTEM-gated op"
273 );
274 }
275 for op in [spawn_op(), schedule_op(), signal_op()] {
276 let ok = authorize(CapabilityMask::SYSTEM, Effect::new(inst(), Principal::System, op));
277 assert!(ok.is_ok(), "System with SYSTEM caps passes every op");
278 }
279 }
280
281 #[test]
282 fn effective_caps_intersects_config_and_ceiling_for_authenticated_principals() {
283 let full = CapabilityMask::all();
286 let none = CapabilityMask::empty();
287 let ext = Principal::External(ExternalId(7));
288 let sys = Principal::System;
289 assert_eq!(effective_caps(full, &ext, none), none, "ceiling caps to none");
290 assert_eq!(effective_caps(none, &ext, full), none, "config caps to none");
291 assert_eq!(effective_caps(full, &ext, full), full);
292 assert_eq!(
294 effective_caps(full, &sys, none),
295 none,
296 "System is bounded by the ceiling — a low ceiling narrows it (no bypass)"
297 );
298 assert_eq!(effective_caps(none, &sys, full), none, "System bounded by config");
299 assert_eq!(effective_caps(full, &sys, full), full);
300 assert_eq!(
302 effective_caps(full, &Principal::Unauthenticated, full),
303 none
304 );
305 }
306
307 #[test]
308 fn scheduled_system_child_cannot_rewiden_past_caps_ceiling() {
309 let full = CapabilityMask::all();
315 let parent_effective = CapabilityMask::SYSTEM; let child = effective_caps(full, &Principal::System, parent_effective);
317 assert_eq!(
318 child, parent_effective,
319 "System child is locked to the parent's effective caps, never default_caps"
320 );
321 assert!(
322 child.bits() <= parent_effective.bits(),
323 "child authority is a subset of the scheduling parent's"
324 );
325 }
326
327 #[test]
328 fn unauthenticated_principal_always_denied() {
329 let e = Effect::new(inst(), Principal::Unauthenticated, spawn_op());
330 let result = authorize(CapabilityMask::SYSTEM, e);
331 assert_eq!(result.unwrap_err(), DenyReason::CapabilityDenied);
332 }
333
334 #[test]
335 fn external_with_system_cap_authorized() {
336 let e = Effect::new(inst(), Principal::External(ExternalId(7)), schedule_op());
337 let result = authorize(CapabilityMask::SYSTEM, e);
338 assert!(result.is_ok());
339 }
340
341 #[test]
342 fn external_without_cap_denied_for_schedule() {
343 let e = Effect::new(inst(), Principal::External(ExternalId(7)), schedule_op());
344 let result = authorize(CapabilityMask::default(), e);
345 assert_eq!(result.unwrap_err(), DenyReason::CapabilityDenied);
346 }
347
348 #[test]
349 fn external_without_cap_denied_for_send_signal() {
350 let e = Effect::new(inst(), Principal::External(ExternalId(7)), signal_op());
351 let result = authorize(CapabilityMask::default(), e);
352 assert_eq!(result.unwrap_err(), DenyReason::CapabilityDenied);
353 }
354
355 #[test]
356 fn external_with_basic_cap_authorized_for_state_op() {
357 let e = Effect::new(inst(), Principal::External(ExternalId(7)), spawn_op());
358 let result = authorize(CapabilityMask::default(), e);
359 assert!(
360 result.is_ok(),
361 "External with basic state op (no SYSTEM) is allowed in MVP"
362 );
363 }
364}