Skip to main content

tsafe_core/
rbac.rs

1//! RBAC access profiles for runtime authority.
2//!
3//! This module keeps the first RBAC slice intentionally small:
4//! - a serializable access profile (`read_only` vs `read_write`)
5//! - explicit capability semantics
6//! - role-scoped derived keys so future vault/team enforcement can bind to
7//!   a stable cryptographic identity without changing the root-key schedule
8//!
9//! Higher layers can carry this profile through contracts, audit context, and
10//! execution policy now; enforcement can build on the same model later.
11
12use serde::{Deserialize, Serialize};
13
14use crate::crypto::{self, VaultKey};
15use crate::errors::{SafeError, SafeResult};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
18#[serde(rename_all = "snake_case")]
19pub enum RbacProfile {
20    ReadOnly,
21    #[default]
22    ReadWrite,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "snake_case")]
27pub enum RbacCapability {
28    Read,
29    Write,
30}
31
32impl RbacProfile {
33    pub fn as_str(self) -> &'static str {
34        match self {
35            Self::ReadOnly => "read_only",
36            Self::ReadWrite => "read_write",
37        }
38    }
39
40    pub fn capabilities(self) -> &'static [RbacCapability] {
41        match self {
42            Self::ReadOnly => &[RbacCapability::Read],
43            Self::ReadWrite => &[RbacCapability::Read, RbacCapability::Write],
44        }
45    }
46
47    pub fn allows_write(self) -> bool {
48        matches!(self, Self::ReadWrite)
49    }
50
51    /// Derive a role-scoped 256-bit key from the root vault key.
52    ///
53    /// This does not change the on-disk vault format by itself. It gives later
54    /// enforcement work a stable, domain-separated key identity for each access
55    /// profile without reusing the root key directly.
56    pub fn derive_role_key(self, root_key: &VaultKey) -> SafeResult<VaultKey> {
57        crypto::derive_labeled_subkey(root_key, self.hkdf_label())
58    }
59
60    pub fn ensure_write_allowed(self) -> SafeResult<()> {
61        if self.allows_write() {
62            Ok(())
63        } else {
64            Err(SafeError::InvalidVault {
65                reason: "rbac access profile 'read_only' does not allow write operations".into(),
66            })
67        }
68    }
69
70    fn hkdf_label(self) -> &'static str {
71        match self {
72            Self::ReadOnly => "tsafe/rbac/read-only/v1",
73            Self::ReadWrite => "tsafe/rbac/read-write/v1",
74        }
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use crate::crypto::{
82        derive_key, random_salt, VAULT_KDF_M_COST, VAULT_KDF_P_COST, VAULT_KDF_T_COST,
83    };
84
85    fn test_root_key() -> VaultKey {
86        let salt = random_salt();
87        derive_key(
88            b"rbac-test-password",
89            &salt,
90            VAULT_KDF_M_COST,
91            VAULT_KDF_T_COST,
92            VAULT_KDF_P_COST,
93        )
94        .unwrap()
95    }
96
97    #[test]
98    fn read_only_profile_is_default_denied_for_write() {
99        assert!(!RbacProfile::ReadOnly.allows_write());
100        assert!(RbacProfile::ReadOnly.ensure_write_allowed().is_err());
101        assert!(RbacProfile::ReadWrite.ensure_write_allowed().is_ok());
102    }
103
104    #[test]
105    fn role_keys_are_domain_separated_and_deterministic() {
106        let root = test_root_key();
107
108        let ro_1 = RbacProfile::ReadOnly.derive_role_key(&root).unwrap();
109        let ro_2 = RbacProfile::ReadOnly.derive_role_key(&root).unwrap();
110        let rw = RbacProfile::ReadWrite.derive_role_key(&root).unwrap();
111
112        assert_eq!(ro_1.as_bytes(), ro_2.as_bytes());
113        assert_ne!(ro_1.as_bytes(), rw.as_bytes());
114    }
115
116    #[test]
117    fn profiles_expose_expected_capabilities() {
118        assert_eq!(
119            RbacProfile::ReadOnly.capabilities(),
120            &[RbacCapability::Read]
121        );
122        assert_eq!(
123            RbacProfile::ReadWrite.capabilities(),
124            &[RbacCapability::Read, RbacCapability::Write]
125        );
126    }
127}