Skip to main content

actionqueue_core/
platform.rs

1//! Platform domain types for multi-tenant isolation, RBAC, and ledger entries.
2//!
3//! These types are consumed by `actionqueue-platform` (for enforcement) and
4//! `actionqueue-storage` (for WAL events and snapshot persistence). They live in
5//! `actionqueue-core` so all crates share a single canonical definition.
6
7use crate::ids::{ActorId, LedgerEntryId, TenantId};
8
9/// Organizational role for an actor within a tenant.
10///
11/// Roles map to capability sets via the RBAC enforcer. The `Custom` variant
12/// allows org-specific extension beyond the standard triad.
13#[derive(Debug, Clone, PartialEq, Eq, Hash)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15pub enum Role {
16    /// Operator: submits work plans, manages tasks.
17    Operator,
18    /// Auditor: reviews plans, produces approval/rejection decisions.
19    Auditor,
20    /// Gatekeeper: executes privileged actions after approval.
21    Gatekeeper,
22    /// Extension role. The inner string must be non-empty.
23    Custom(String),
24}
25
26impl Role {
27    /// Creates a validated custom role.
28    ///
29    /// # Errors
30    ///
31    /// Returns an error if `name` is empty.
32    pub fn custom(name: impl Into<String>) -> Result<Self, String> {
33        let name = name.into();
34        if name.is_empty() {
35            return Err("custom role name must be non-empty".to_string());
36        }
37        Ok(Role::Custom(name))
38    }
39}
40
41/// Typed permission for an actor within a tenant.
42#[derive(Debug, Clone, PartialEq, Eq, Hash)]
43#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
44pub enum Capability {
45    /// Actor may submit new tasks.
46    CanSubmit,
47    /// Actor may execute (claim and complete) tasks.
48    CanExecute,
49    /// Actor may review tasks and produce approval/rejection outcomes.
50    CanReview,
51    /// Actor may approve proposed actions.
52    CanApprove,
53    /// Actor may cancel tasks and runs.
54    CanCancel,
55    /// Extension capability. The inner string must be non-empty.
56    Custom(String),
57}
58
59impl Capability {
60    /// Creates a validated custom capability.
61    ///
62    /// # Errors
63    ///
64    /// Returns an error if `name` is empty.
65    pub fn custom(name: impl Into<String>) -> Result<Self, String> {
66        let name = name.into();
67        if name.is_empty() {
68            return Err("custom capability name must be non-empty".to_string());
69        }
70        Ok(Capability::Custom(name))
71    }
72}
73
74/// Tenant registration record.
75#[derive(Debug, Clone, PartialEq, Eq)]
76#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
77pub struct TenantRegistration {
78    tenant_id: TenantId,
79    name: String,
80}
81
82impl TenantRegistration {
83    /// Creates a new tenant registration.
84    ///
85    /// # Panics
86    ///
87    /// Panics if `name` is empty.
88    pub fn new(tenant_id: TenantId, name: impl Into<String>) -> Self {
89        let name = name.into();
90        assert!(!name.is_empty(), "tenant name must be non-empty");
91        TenantRegistration { tenant_id, name }
92    }
93
94    /// Returns the tenant identifier.
95    pub fn tenant_id(&self) -> TenantId {
96        self.tenant_id
97    }
98
99    /// Returns the tenant name.
100    pub fn name(&self) -> &str {
101        &self.name
102    }
103}
104
105/// Generic ledger entry for append-only platform ledgers.
106///
107/// Ledger keys identify the logical ledger (e.g. `"audit"`, `"decision"`,
108/// `"relationship"`, `"incident"`, `"reality"`). The payload is opaque bytes
109/// whose schema is defined by the consumer (Caelum, Digicorp).
110#[derive(Debug, Clone, PartialEq, Eq)]
111#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
112pub struct LedgerEntry {
113    entry_id: LedgerEntryId,
114    tenant_id: TenantId,
115    /// The logical ledger name (e.g. `"audit"`, `"decision"`).
116    ledger_key: String,
117    /// The actor that produced this entry, if any.
118    actor_id: Option<ActorId>,
119    /// Opaque payload bytes. Schema is consumer-defined.
120    payload: Vec<u8>,
121    /// Unix epoch seconds when this entry was recorded.
122    timestamp: u64,
123}
124
125impl LedgerEntry {
126    /// Creates a new ledger entry.
127    ///
128    /// `actor_id` can be set via [`with_actor`](Self::with_actor) after construction.
129    ///
130    /// # Panics
131    ///
132    /// Panics if `ledger_key` is empty.
133    pub fn new(
134        entry_id: LedgerEntryId,
135        tenant_id: TenantId,
136        ledger_key: impl Into<String>,
137        payload: Vec<u8>,
138        timestamp: u64,
139    ) -> Self {
140        let ledger_key = ledger_key.into();
141        assert!(!ledger_key.is_empty(), "ledger_key must be non-empty");
142        LedgerEntry { entry_id, tenant_id, ledger_key, actor_id: None, payload, timestamp }
143    }
144
145    /// Attaches an actor identifier, returning the modified entry.
146    pub fn with_actor(mut self, actor_id: ActorId) -> Self {
147        self.actor_id = Some(actor_id);
148        self
149    }
150
151    /// Returns the entry identifier.
152    pub fn entry_id(&self) -> LedgerEntryId {
153        self.entry_id
154    }
155
156    /// Returns the tenant identifier.
157    pub fn tenant_id(&self) -> TenantId {
158        self.tenant_id
159    }
160
161    /// Returns the logical ledger key.
162    pub fn ledger_key(&self) -> &str {
163        &self.ledger_key
164    }
165
166    /// Returns the actor identifier, if any.
167    pub fn actor_id(&self) -> Option<ActorId> {
168        self.actor_id
169    }
170
171    /// Returns the opaque payload bytes.
172    pub fn payload(&self) -> &[u8] {
173        &self.payload
174    }
175
176    /// Returns the entry timestamp (Unix epoch seconds).
177    pub fn timestamp(&self) -> u64 {
178        self.timestamp
179    }
180}