From dece4dcc60562e60169bebe913be3a588c2a02ca Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 13:19:53 +0000 Subject: [PATCH] ogar-rbac: IdentityActors answers through the contract's SubjectRoles The one-subject rule (the subject holds its roles, every other actor, the acting party of a delegated login included, holds none) now lives once in lance-graph-contract's SubjectRoles; IdentityActors uses it instead of its own comparison. One behaviour change: a user whose subject is empty holds no role, so an actor id that also failed to resolve can never pick it up. an_empty_subject_holds_no_role pins it. HubSPO wishlist #11. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01G22yT6htkcdyXsihxxXdrg --- crates/ogar-rbac/src/lib.rs | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/crates/ogar-rbac/src/lib.rs b/crates/ogar-rbac/src/lib.rs index c721cae..c549f8f 100644 --- a/crates/ogar-rbac/src/lib.rs +++ b/crates/ogar-rbac/src/lib.rs @@ -63,7 +63,8 @@ use lance_graph_contract::rbac::{ ActorId, ClassGrant, ClassId, ClassRbac, Operation, RoleId, ScopeSpec, grants_permit, }; use lance_graph_contract::rbac_plug::{ - ActorSource, RbacAuthority, RbacBinding, RbacDrift, RbacPlug, verify_concepts_against_mirror, + ActorSource, RbacAuthority, RbacBinding, RbacDrift, RbacPlug, SubjectRoles, + verify_concepts_against_mirror, }; use lance_graph_rbac::authorize::{MembershipDecision, authorize_memberships}; use lance_graph_rbac::authorize::{ScopedDecision, authorize_scoped}; @@ -227,10 +228,9 @@ impl RbacAuthority for OgarRbac { /// role here, under any name. #[derive(Debug, Clone, PartialEq, Eq)] pub struct IdentityActors { - subject: String, + held: SubjectRoles, acting: Option, tenant: u64, - roles: Vec, unplugged: Vec, } @@ -252,10 +252,9 @@ impl IdentityActors { } } Self { - subject: identity.user.subject.clone(), + held: SubjectRoles::new(identity.user.subject.clone(), roles), acting: identity.acting_party().cloned(), tenant: identity.user.tenant, - roles, unplugged, } } @@ -277,11 +276,7 @@ impl IdentityActors { impl ActorSource for IdentityActors { fn roles_of(&self, actor: ActorId<'_>) -> &[RoleId] { - if actor == self.subject { - &self.roles - } else { - &[] - } + self.held.of(actor) } fn memberships_of(&self, actor: ActorId<'_>, _class: ClassId) -> Vec { @@ -742,6 +737,18 @@ mod tests { ); } + // A user whose subject did not resolve holds nothing, so an actor id + // that also failed to resolve can never pick up its roles. + #[test] + fn an_empty_subject_holds_no_role() { + let binding = authority().bind(&PLUG).expect("green bind"); + let mut user = identity(&["physician"], 7); + user.user.subject = String::new(); + let actors = IdentityActors::new(&user, &binding); + assert!(actors.roles_of("").is_empty()); + assert!(actors.memberships_of("", patient_class()).is_empty()); + } + // A delegated login (RFC 8693 `act`): bob acts with dr-house's authority. // The decision is dr-house's; bob is recorded and holds nothing himself. #[test]