use serde::{Deserialize, Serialize};
use crate::crypto::{self, VaultKey};
use crate::errors::{SafeError, SafeResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum RbacProfile {
ReadOnly,
#[default]
ReadWrite,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RbacCapability {
Read,
Write,
}
impl RbacProfile {
pub fn as_str(self) -> &'static str {
match self {
Self::ReadOnly => "read_only",
Self::ReadWrite => "read_write",
}
}
pub fn capabilities(self) -> &'static [RbacCapability] {
match self {
Self::ReadOnly => &[RbacCapability::Read],
Self::ReadWrite => &[RbacCapability::Read, RbacCapability::Write],
}
}
pub fn allows_write(self) -> bool {
matches!(self, Self::ReadWrite)
}
pub fn derive_role_key(self, root_key: &VaultKey) -> SafeResult<VaultKey> {
crypto::derive_labeled_subkey(root_key, self.hkdf_label())
}
pub fn ensure_write_allowed(self) -> SafeResult<()> {
if self.allows_write() {
Ok(())
} else {
Err(SafeError::InvalidVault {
reason: "rbac access profile 'read_only' does not allow write operations".into(),
})
}
}
fn hkdf_label(self) -> &'static str {
match self {
Self::ReadOnly => "tsafe/rbac/read-only/v1",
Self::ReadWrite => "tsafe/rbac/read-write/v1",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::{
derive_key, random_salt, VAULT_KDF_M_COST, VAULT_KDF_P_COST, VAULT_KDF_T_COST,
};
fn test_root_key() -> VaultKey {
let salt = random_salt();
derive_key(
b"rbac-test-password",
&salt,
VAULT_KDF_M_COST,
VAULT_KDF_T_COST,
VAULT_KDF_P_COST,
)
.unwrap()
}
#[test]
fn read_only_profile_is_default_denied_for_write() {
assert!(!RbacProfile::ReadOnly.allows_write());
assert!(RbacProfile::ReadOnly.ensure_write_allowed().is_err());
assert!(RbacProfile::ReadWrite.ensure_write_allowed().is_ok());
}
#[test]
fn role_keys_are_domain_separated_and_deterministic() {
let root = test_root_key();
let ro_1 = RbacProfile::ReadOnly.derive_role_key(&root).unwrap();
let ro_2 = RbacProfile::ReadOnly.derive_role_key(&root).unwrap();
let rw = RbacProfile::ReadWrite.derive_role_key(&root).unwrap();
assert_eq!(ro_1.as_bytes(), ro_2.as_bytes());
assert_ne!(ro_1.as_bytes(), rw.as_bytes());
}
#[test]
fn profiles_expose_expected_capabilities() {
assert_eq!(
RbacProfile::ReadOnly.capabilities(),
&[RbacCapability::Read]
);
assert_eq!(
RbacProfile::ReadWrite.capabilities(),
&[RbacCapability::Read, RbacCapability::Write]
);
}
}