//! Domain types for the ReBAC authorization model. //! //! These types are storage-agnostic — they describe the relationship between //! a subject (who), a resource (what), and a permission (action). The //! `AuthorizationEngine` port consumes them and the `PgAclEngine` implementation //! maps them to / from `storage.access_grants` rows. use uuid::Uuid; // ════════════════════════════════════════════════════════════════════════════ // Subject — who has the permission // ════════════════════════════════════════════════════════════════════════════ /// A principal that can be granted permissions. /// /// All variants carry a `Uuid` that uniquely identifies the subject within /// its type's namespace. #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub enum Subject { /// A registered OxiCloud user (`auth.users.id`). User(Uuid), /// A user group (reserved for future use; no group CRUD in v1). Group(Uuid), /// An anonymous share token (`storage.shares.id`). Token(Uuid), /// A federated identity from another server — Open Cloud Mesh, external /// OIDC, etc. Refers to `auth.external_subjects.id` (future table). External(Uuid), } impl Subject { /// SQL discriminator string matching the `subject_type` CHECK constraint. pub fn type_str(&self) -> &'static str { match self { Subject::User(_) => "user", Subject::Group(_) => "group", Subject::Token(_) => "token", Subject::External(_) => "external", } } /// The raw UUID regardless of variant. pub fn id(&self) -> Uuid { match self { Subject::User(id) | Subject::Group(id) | Subject::Token(id) | Subject::External(id) => { *id } } } /// Reconstruct from a SQL row's `(subject_type, subject_id)` pair. pub fn from_parts(subject_type: &str, id: Uuid) -> Option { match subject_type { "user" => Some(Subject::User(id)), "group" => Some(Subject::Group(id)), "token" => Some(Subject::Token(id)), "external" => Some(Subject::External(id)), _ => None, } } } // ════════════════════════════════════════════════════════════════════════════ // Resource — what the permission is on // ════════════════════════════════════════════════════════════════════════════ #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub enum Resource { Folder(Uuid), File(Uuid), } impl Resource { pub fn type_str(&self) -> &'static str { match self { Resource::Folder(_) => "folder", Resource::File(_) => "file", } } pub fn id(&self) -> Uuid { match self { Resource::Folder(id) | Resource::File(id) => *id, } } pub fn from_parts(resource_type: &str, id: Uuid) -> Option { match resource_type { "folder" => Some(Resource::Folder(id)), "file" => Some(Resource::File(id)), _ => None, } } } // ════════════════════════════════════════════════════════════════════════════ // Permission — what action is allowed // ════════════════════════════════════════════════════════════════════════════ #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub enum Permission { /// View resource content / list folder contents. Read, /// Create child resources inside (only meaningful on folders). Create, /// Grant permissions to other subjects. Share, /// Add comments (reserved — comments feature not implemented yet). Comment, /// Delete the resource. Delete, /// Modify the resource (rename, move, edit content). Update, } impl Permission { /// Every permission, in a stable order. Used by `Role::expand()` and SQL /// `permission = ANY(...)` lookups. pub const ALL: [Permission; 6] = [ Permission::Read, Permission::Create, Permission::Share, Permission::Comment, Permission::Delete, Permission::Update, ]; pub fn as_str(&self) -> &'static str { match self { Permission::Read => "read", Permission::Create => "create", Permission::Share => "share", Permission::Comment => "comment", Permission::Delete => "delete", Permission::Update => "update", } } /// Parse a permission from its SQL discriminator string. Returns None /// for unknown values. pub fn parse(s: &str) -> Option { match s { "read" => Some(Permission::Read), "create" => Some(Permission::Create), "share" => Some(Permission::Share), "comment" => Some(Permission::Comment), "delete" => Some(Permission::Delete), "update" => Some(Permission::Update), _ => None, } } } // ════════════════════════════════════════════════════════════════════════════ // Grant — a row in storage.access_grants // ════════════════════════════════════════════════════════════════════════════ #[derive(Clone, Debug)] pub struct Grant { pub id: Uuid, pub subject: Subject, pub resource: Resource, pub permission: Permission, pub granted_by: Uuid, pub granted_at: chrono::DateTime, } #[cfg(test)] mod tests { use super::*; #[test] fn subject_roundtrip() { let id = Uuid::new_v4(); let cases = [ Subject::User(id), Subject::Group(id), Subject::Token(id), Subject::External(id), ]; for s in cases { let back = Subject::from_parts(s.type_str(), s.id()).unwrap(); assert_eq!(s, back); } assert!(Subject::from_parts("unknown", id).is_none()); } #[test] fn resource_roundtrip() { let id = Uuid::new_v4(); for r in [Resource::Folder(id), Resource::File(id)] { let back = Resource::from_parts(r.type_str(), r.id()).unwrap(); assert_eq!(r, back); } assert!(Resource::from_parts("calendar", id).is_none()); } #[test] fn permission_roundtrip() { for p in Permission::ALL { assert_eq!(Permission::parse(p.as_str()), Some(p)); } assert!(Permission::parse("administrate").is_none()); } }