|
| 1 | +//! `rbac` — the classid-keyed authorization trait surface (OGAR keystone §4/§11). |
| 2 | +//! |
| 3 | +//! The keystone §11 build order places the **`ClassRbac` grant-resolution trait** |
| 4 | +//! in this zero-dep contract crate so that *both* the concrete kernel |
| 5 | +//! (`lance-graph-rbac`, which holds `authorize()` + `Policy` + the `0x0B` auth |
| 6 | +//! membrane) *and* the active-record `ClassView` producer (`lance-graph-ogar`'s |
| 7 | +//! `OgarClassView`, which deps contract but **not** rbac) can implement / consume |
| 8 | +//! one trait. Before this module the trait lived in `lance-graph-rbac`, so ogar — |
| 9 | +//! which does not depend on rbac — could not satisfy the keystone's |
| 10 | +//! `impl ClassRbac for OgarClassView` (Q5). This is that placement. |
| 11 | +//! |
| 12 | +//! Only the **trait + the `Operation` it ranges over** live here (pure types, no |
| 13 | +//! runtime — `Operation` reads [`PrefetchDepth`](crate::property::PrefetchDepth), |
| 14 | +//! already in this crate). The concrete `authorize()` kernel, `ClassGrants`, |
| 15 | +//! `Policy`, `AccessDecision`, and the auth membrane stay in `lance-graph-rbac`; |
| 16 | +//! it **re-exports** these so existing `lance_graph_rbac::authorize::ClassRbac` / |
| 17 | +//! `lance_graph_rbac::policy::Operation` paths are unchanged (callcenter + |
| 18 | +//! the sibling `smb-realtime` / `medcare-realtime` gates keep compiling). |
| 19 | +//! |
| 20 | +//! # Relationship to the rest of the contract auth surface |
| 21 | +//! |
| 22 | +//! - [`crate::auth::ActorContext`] is the *resolved actor identity* (actor id + |
| 23 | +//! tenant + roles). `lance-graph-rbac`'s `auth::ResolvedIdentity` (the `0x0B` |
| 24 | +//! membrane output) carries the same triple plus the resolving provider's |
| 25 | +//! classid; converging the two onto `ActorContext` is a tracked follow-on, not |
| 26 | +//! forced here. |
| 27 | +//! - [`crate::external_membrane::MembraneGate`] is the *gate* a consumer impls to |
| 28 | +//! admit/deny an external commit; `ClassRbac` is the *grant resolution* a gate |
| 29 | +//! consults. They compose: a gate calls `authorize(rbac, actor, class, op)`. |
| 30 | +
|
| 31 | +use crate::property::PrefetchDepth; |
| 32 | + |
| 33 | +/// The codebook class identity an authorization targets — the |
| 34 | +/// [`NodeGuid`](crate::NodeGuid) `classid` (or its low-`u16` codebook id widened). |
| 35 | +/// Opaque to the kernel: it is compared and looked up, never decoded (the kernel |
| 36 | +/// "never touches a token" — only resolved keys go inward). |
| 37 | +pub type ClassId = u32; |
| 38 | + |
| 39 | +/// An actor identity. In the full keystone this is the OIDC `sub` resolved to a |
| 40 | +/// membership-set ([`crate::auth::ActorContext`]); here it is the opaque key a |
| 41 | +/// [`ClassRbac`] impl maps to roles. |
| 42 | +pub type ActorId<'a> = &'a str; |
| 43 | + |
| 44 | +/// A role identity (a minted role classid in the full keystone; a role *name* |
| 45 | +/// where reconciling against a string-keyed policy). |
| 46 | +pub type RoleId = &'static str; |
| 47 | + |
| 48 | +/// What a caller wants to do on a class — the op the [`ClassRbac`] grant gate |
| 49 | +/// ranges over. Read is depth-graded ([`PrefetchDepth`]); Write names a |
| 50 | +/// predicate; Act names an action. (Promoted from `lance-graph-rbac`'s |
| 51 | +/// `policy::Operation`, keystone §11; that path re-exports this type.) |
| 52 | +#[derive(Clone, Debug)] |
| 53 | +pub enum Operation<'a> { |
| 54 | + /// Read up to a prefetch depth. |
| 55 | + Read { |
| 56 | + /// The requested read depth (`Identity` < … < `Full`). |
| 57 | + depth: PrefetchDepth, |
| 58 | + }, |
| 59 | + /// Write a specific predicate. |
| 60 | + Write { |
| 61 | + /// The predicate being written. |
| 62 | + predicate: &'a str, |
| 63 | + }, |
| 64 | + /// Trigger a named action. |
| 65 | + Act { |
| 66 | + /// The action name. |
| 67 | + action: &'a str, |
| 68 | + }, |
| 69 | +} |
| 70 | + |
| 71 | +/// The §4 grant-resolution surface, **classid-keyed**. The single trait both the |
| 72 | +/// membrane gate and the cognitive loop resolve access through; the impl owns the |
| 73 | +/// membership→role folding and the `(role, class)` grant table. `lance-graph-rbac` |
| 74 | +/// supplies the reference impl (`ClassGrants`) + the `authorize()` kernel that |
| 75 | +/// consumes it; `lance-graph-ogar`'s `OgarClassView` is the keystone's intended |
| 76 | +/// active-record impl (Q5). |
| 77 | +pub trait ClassRbac { |
| 78 | + /// Roles the actor holds, already folded through |
| 79 | + /// membership → member_role → role (the §4 `actor_roles`). Empty ⇒ the actor |
| 80 | + /// is unknown to the policy. |
| 81 | + fn actor_roles(&self, actor: ActorId<'_>) -> &[RoleId]; |
| 82 | + |
| 83 | + /// Does `role` carry a grant on `class` that permits `op`? The positive |
| 84 | + /// `R⁺` op-mask gate (§5 stage 1). No grant, or a grant that does not permit |
| 85 | + /// the op, ⇒ `false` (restrictive default-deny). |
| 86 | + fn grant_permits(&self, role: RoleId, class: ClassId, op: &Operation<'_>) -> bool; |
| 87 | +} |
| 88 | + |
| 89 | +#[cfg(test)] |
| 90 | +mod tests { |
| 91 | + use super::*; |
| 92 | + |
| 93 | + #[test] |
| 94 | + fn operation_reads_prefetch_depth() { |
| 95 | + // Operation ranges over the contract's own PrefetchDepth — no rbac dep. |
| 96 | + let op = Operation::Read { |
| 97 | + depth: PrefetchDepth::Full, |
| 98 | + }; |
| 99 | + assert!(matches!(op, Operation::Read { .. })); |
| 100 | + } |
| 101 | + |
| 102 | + // A trivial in-contract ClassRbac impl proves the trait is satisfiable with |
| 103 | + // contract-only types (the property ogar relies on: deps contract, not rbac). |
| 104 | + struct OneRole; |
| 105 | + impl ClassRbac for OneRole { |
| 106 | + fn actor_roles(&self, _actor: ActorId<'_>) -> &[RoleId] { |
| 107 | + const R: &[RoleId] = &["reader"]; |
| 108 | + R |
| 109 | + } |
| 110 | + fn grant_permits(&self, role: RoleId, class: ClassId, op: &Operation<'_>) -> bool { |
| 111 | + role == "reader" && class == 0x0901 && matches!(op, Operation::Read { .. }) |
| 112 | + } |
| 113 | + } |
| 114 | + |
| 115 | + #[test] |
| 116 | + fn trait_is_satisfiable_with_contract_only_types() { |
| 117 | + let rbac = OneRole; |
| 118 | + assert_eq!(rbac.actor_roles("anyone"), &["reader"]); |
| 119 | + assert!(rbac.grant_permits( |
| 120 | + "reader", |
| 121 | + 0x0901, |
| 122 | + &Operation::Read { |
| 123 | + depth: PrefetchDepth::Identity |
| 124 | + } |
| 125 | + )); |
| 126 | + assert!(!rbac.grant_permits("reader", 0x0901, &Operation::Act { action: "x" })); |
| 127 | + } |
| 128 | +} |
0 commit comments