use bitflags::bitflags;
use serde::{Deserialize, Serialize};
use crate::dbms::table::TableFingerprint;
bitflags! {
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TablePerms: u8 {
const READ = 0b0001;
const INSERT = 0b0010;
const UPDATE = 0b0100;
const DELETE = 0b1000;
}
}
#[cfg(feature = "candid")]
impl candid::CandidType for TablePerms {
fn _ty() -> candid::types::Type {
candid::types::TypeInner::Nat8.into()
}
fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
where
S: candid::types::Serializer,
{
serializer.serialize_nat8(self.bits())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "candid", derive(candid::CandidType))]
pub enum RequiredPerm {
Table(TablePerms),
Admin,
ManageAcl,
Migrate,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "candid", derive(candid::CandidType))]
pub struct IdentityPerms {
pub admin: bool,
pub manage_acl: bool,
pub migrate: bool,
pub all_tables: TablePerms,
pub per_table: Vec<(TableFingerprint, TablePerms)>,
}
impl IdentityPerms {
pub fn fully_permissive() -> Self {
Self {
admin: true,
manage_acl: true,
migrate: true,
all_tables: TablePerms::all(),
per_table: Vec::new(),
}
}
pub fn grants_table(&self, table: TableFingerprint, required: TablePerms) -> bool {
if self.admin {
return true;
}
let union = self.all_tables | self.lookup(table);
union.contains(required)
}
fn lookup(&self, table: TableFingerprint) -> TablePerms {
self.per_table
.iter()
.find(|(t, _)| *t == table)
.map(|(_, p)| *p)
.unwrap_or_default()
}
pub fn apply_grant(&mut self, grant: PermGrant) {
match grant {
PermGrant::Admin => self.admin = true,
PermGrant::ManageAcl => self.manage_acl = true,
PermGrant::Migrate => self.migrate = true,
PermGrant::AllTables(p) => self.all_tables |= p,
PermGrant::Table(t, p) => match self.per_table.iter_mut().find(|(tt, _)| *tt == t) {
Some((_, existing)) => *existing |= p,
None => self.per_table.push((t, p)),
},
}
}
pub fn apply_revoke(&mut self, revoke: PermRevoke) {
match revoke {
PermRevoke::Admin => self.admin = false,
PermRevoke::ManageAcl => self.manage_acl = false,
PermRevoke::Migrate => self.migrate = false,
PermRevoke::AllTables(p) => self.all_tables.remove(p),
PermRevoke::Table(t, p) => {
if let Some(pos) = self.per_table.iter().position(|(tt, _)| *tt == t) {
self.per_table[pos].1.remove(p);
if self.per_table[pos].1.is_empty() {
self.per_table.swap_remove(pos);
}
}
}
}
}
pub fn is_empty(&self) -> bool {
!self.admin
&& !self.manage_acl
&& !self.migrate
&& self.all_tables.is_empty()
&& self.per_table.is_empty()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "candid", derive(candid::CandidType))]
pub enum PermGrant {
Admin,
ManageAcl,
Migrate,
AllTables(TablePerms),
Table(TableFingerprint, TablePerms),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "candid", derive(candid::CandidType))]
pub enum PermRevoke {
Admin,
ManageAcl,
Migrate,
AllTables(TablePerms),
Table(TableFingerprint, TablePerms),
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dbms::table::fingerprint_for_name;
fn fp(name: &str) -> TableFingerprint {
fingerprint_for_name(name)
}
#[test]
fn test_admin_bypasses_table_check() {
let mut p = IdentityPerms::default();
p.admin = true;
assert!(p.grants_table(fp("users"), TablePerms::DELETE));
}
#[test]
fn test_all_tables_grant_unions_with_per_table() {
let mut p = IdentityPerms::default();
p.all_tables = TablePerms::READ;
p.apply_grant(PermGrant::Table(
fp("users"),
TablePerms::INSERT | TablePerms::UPDATE,
));
assert!(p.grants_table(fp("users"), TablePerms::READ));
assert!(p.grants_table(fp("users"), TablePerms::INSERT));
assert!(!p.grants_table(fp("users"), TablePerms::DELETE));
assert!(p.grants_table(fp("posts"), TablePerms::READ));
assert!(!p.grants_table(fp("posts"), TablePerms::INSERT));
}
#[test]
fn test_apply_grant_is_idempotent() {
let mut p = IdentityPerms::default();
p.apply_grant(PermGrant::Admin);
p.apply_grant(PermGrant::Admin);
assert!(p.admin);
p.apply_grant(PermGrant::Table(fp("users"), TablePerms::READ));
p.apply_grant(PermGrant::Table(fp("users"), TablePerms::READ));
assert_eq!(p.per_table.len(), 1);
assert_eq!(p.per_table[0], (fp("users"), TablePerms::READ));
}
#[test]
fn test_revoke_partial_table_bits() {
let mut p = IdentityPerms::default();
p.apply_grant(PermGrant::Table(
fp("users"),
TablePerms::READ | TablePerms::INSERT | TablePerms::DELETE,
));
p.apply_revoke(PermRevoke::Table(
fp("users"),
TablePerms::INSERT | TablePerms::DELETE,
));
assert_eq!(p.per_table[0].1, TablePerms::READ);
}
#[test]
fn test_revoke_all_table_bits_removes_entry() {
let mut p = IdentityPerms::default();
p.apply_grant(PermGrant::Table(fp("users"), TablePerms::READ));
p.apply_revoke(PermRevoke::Table(fp("users"), TablePerms::READ));
assert!(p.per_table.is_empty());
}
#[test]
fn test_admin_does_not_imply_manage_acl_or_migrate() {
let mut p = IdentityPerms::default();
p.admin = true;
assert!(!p.manage_acl);
assert!(!p.migrate);
}
#[test]
fn test_fully_permissive_grants_everything() {
let p = IdentityPerms::fully_permissive();
assert!(p.admin && p.manage_acl && p.migrate);
assert!(p.grants_table(fp("anything"), TablePerms::all()));
}
#[test]
fn test_is_empty_after_revoking_all() {
let mut p = IdentityPerms::default();
p.apply_grant(PermGrant::Admin);
assert!(!p.is_empty());
p.apply_revoke(PermRevoke::Admin);
assert!(p.is_empty());
}
}