use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Permission {
Read,
Write,
Update,
Delete,
Query,
CreateIndex,
DeleteIndex,
ManageCollections,
ViewStats,
ManageUsers,
Admin,
}
impl Permission {
pub fn implies(&self, other: &Permission) -> bool {
match self {
Permission::Admin => true, Permission::Write => matches!(other, Permission::Read | Permission::Write),
Permission::Update => matches!(other, Permission::Read | Permission::Update),
Permission::Delete => matches!(other, Permission::Read | Permission::Delete),
_ => self == other,
}
}
pub fn all() -> Vec<Permission> {
vec![
Permission::Read,
Permission::Write,
Permission::Update,
Permission::Delete,
Permission::Query,
Permission::CreateIndex,
Permission::DeleteIndex,
Permission::ManageCollections,
Permission::ViewStats,
Permission::ManageUsers,
Permission::Admin,
]
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Resource {
Vector(String),
Collection(String),
Index(String),
Namespace(String),
Global,
}
impl Resource {
pub fn contains(&self, other: &Resource) -> bool {
match (self, other) {
(Resource::Global, _) => true,
(Resource::Namespace(ns1), Resource::Collection(col)) => col.starts_with(ns1),
(Resource::Collection(col1), Resource::Vector(vec)) => vec.starts_with(col1),
_ => self == other,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Role {
pub name: String,
pub permissions: HashSet<Permission>,
pub description: String,
pub inherits_from: Vec<String>,
}
impl Role {
pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
name: name.into(),
permissions: HashSet::new(),
description: description.into(),
inherits_from: Vec::new(),
}
}
pub fn with_permission(mut self, permission: Permission) -> Self {
self.permissions.insert(permission);
self
}
pub fn with_permissions(mut self, permissions: Vec<Permission>) -> Self {
self.permissions.extend(permissions);
self
}
pub fn inherits_from(mut self, role_name: impl Into<String>) -> Self {
self.inherits_from.push(role_name.into());
self
}
pub fn has_permission(&self, permission: &Permission) -> bool {
self.permissions.iter().any(|p| p.implies(permission))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Policy {
pub id: String,
pub subject: String,
pub resource: Resource,
pub permission: Permission,
pub effect: Effect,
pub conditions: Vec<Condition>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Effect {
Allow,
Deny,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Condition {
pub attribute: String,
pub operator: Operator,
pub value: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Operator {
Equals,
NotEquals,
Contains,
StartsWith,
EndsWith,
GreaterThan,
LessThan,
}
impl Condition {
pub fn evaluate(&self, context: &AccessContext) -> bool {
let actual = context.attributes.get(&self.attribute);
match actual {
Some(actual_value) => self.check_operator(actual_value),
None => false,
}
}
fn check_operator(&self, actual: &str) -> bool {
match self.operator {
Operator::Equals => actual == self.value,
Operator::NotEquals => actual != self.value,
Operator::Contains => actual.contains(&self.value),
Operator::StartsWith => actual.starts_with(&self.value),
Operator::EndsWith => actual.ends_with(&self.value),
Operator::GreaterThan => actual > self.value.as_str(),
Operator::LessThan => actual < self.value.as_str(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct AccessContext {
pub attributes: HashMap<String, String>,
}
impl AccessContext {
pub fn new() -> Self {
Self::default()
}
pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.attributes.insert(key.into(), value.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub id: String,
pub roles: Vec<String>,
pub attributes: HashMap<String, String>,
}
impl User {
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
roles: Vec::new(),
attributes: HashMap::new(),
}
}
pub fn with_role(mut self, role: impl Into<String>) -> Self {
self.roles.push(role.into());
self
}
pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.attributes.insert(key.into(), value.into());
self
}
}
pub struct AccessControl {
roles: HashMap<String, Role>,
users: HashMap<String, User>,
policies: Vec<Policy>,
default_effect: Effect,
}
impl AccessControl {
pub fn new() -> Self {
let mut ac = Self {
roles: HashMap::new(),
users: HashMap::new(),
policies: Vec::new(),
default_effect: Effect::Deny,
};
ac.add_role(Self::viewer_role());
ac.add_role(Self::editor_role());
ac.add_role(Self::admin_role());
ac
}
pub fn add_role(&mut self, role: Role) {
self.roles.insert(role.name.clone(), role);
}
pub fn add_user(&mut self, user: User) {
self.users.insert(user.id.clone(), user);
}
pub fn add_policy(&mut self, policy: Policy) {
self.policies.push(policy);
}
pub fn check_permission(
&self,
user_id: &str,
resource: &Resource,
permission: &Permission,
context: &AccessContext,
) -> bool {
let user = match self.users.get(user_id) {
Some(u) => u,
None => return false,
};
let mut user_permissions = HashSet::new();
for role_name in &user.roles {
if let Some(role) = self.roles.get(role_name) {
self.collect_permissions(role, &mut user_permissions);
}
}
let has_permission = user_permissions.iter().any(|p| p.implies(permission));
if !has_permission {
return false;
}
self.evaluate_policies(user_id, resource, permission, context)
}
fn collect_permissions(&self, role: &Role, permissions: &mut HashSet<Permission>) {
permissions.extend(&role.permissions);
for parent_name in &role.inherits_from {
if let Some(parent) = self.roles.get(parent_name) {
self.collect_permissions(parent, permissions);
}
}
}
fn evaluate_policies(
&self,
user_id: &str,
resource: &Resource,
permission: &Permission,
context: &AccessContext,
) -> bool {
let mut deny = false;
for policy in &self.policies {
if !self.policy_applies(policy, user_id, resource, permission) {
continue;
}
if !policy.conditions.iter().all(|c| c.evaluate(context)) {
continue;
}
if matches!(policy.effect, Effect::Deny) {
deny = true;
}
}
if deny {
return false;
}
true
}
fn policy_applies(
&self,
policy: &Policy,
user_id: &str,
resource: &Resource,
permission: &Permission,
) -> bool {
let subject_matches = if policy.subject == user_id {
true
} else {
if let Some(user) = self.users.get(user_id) {
user.roles.contains(&policy.subject)
} else {
false
}
};
if !subject_matches {
return false;
}
let resource_matches = policy.resource.contains(resource) || policy.resource == *resource;
if !resource_matches {
return false;
}
policy.permission.implies(permission)
}
pub fn get_user_permissions(&self, user_id: &str, resource: &Resource) -> Vec<Permission> {
let user = match self.users.get(user_id) {
Some(u) => u,
None => return Vec::new(),
};
let mut permissions = HashSet::new();
for role_name in &user.roles {
if let Some(role) = self.roles.get(role_name) {
self.collect_permissions(role, &mut permissions);
}
}
let context = AccessContext::new();
permissions
.into_iter()
.filter(|p| self.check_permission(user_id, resource, p, &context))
.collect()
}
fn viewer_role() -> Role {
Role::new("viewer", "Can read and query vectors").with_permissions(vec![
Permission::Read,
Permission::Query,
Permission::ViewStats,
])
}
fn editor_role() -> Role {
Role::new("editor", "Can read, write, and modify vectors")
.with_permissions(vec![
Permission::Read,
Permission::Write,
Permission::Update,
Permission::Query,
Permission::ViewStats,
])
.inherits_from("viewer")
}
fn admin_role() -> Role {
Role::new("admin", "Full administrative access").with_permission(Permission::Admin)
}
}
impl Default for AccessControl {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_permission_implies() {
assert!(Permission::Admin.implies(&Permission::Read));
assert!(Permission::Admin.implies(&Permission::Write));
assert!(Permission::Write.implies(&Permission::Read));
assert!(!Permission::Read.implies(&Permission::Write));
}
#[test]
fn test_resource_hierarchy() {
let global = Resource::Global;
let namespace = Resource::Namespace("ns1".to_string());
let collection = Resource::Collection("ns1/col1".to_string());
let vector = Resource::Vector("ns1/col1/vec1".to_string());
assert!(global.contains(&namespace));
assert!(global.contains(&collection));
assert!(namespace.contains(&collection));
assert!(collection.contains(&vector));
assert!(!collection.contains(&namespace));
}
#[test]
fn test_role_permissions() {
let role = Role::new("test", "Test role")
.with_permission(Permission::Read)
.with_permission(Permission::Write);
assert!(role.has_permission(&Permission::Read));
assert!(role.has_permission(&Permission::Write));
assert!(!role.has_permission(&Permission::Delete));
}
#[test]
fn test_basic_access_control() {
let mut ac = AccessControl::new();
let user = User::new("alice").with_role("viewer");
ac.add_user(user);
let context = AccessContext::new();
assert!(ac.check_permission("alice", &Resource::Global, &Permission::Read, &context));
assert!(!ac.check_permission("alice", &Resource::Global, &Permission::Write, &context));
}
#[test]
fn test_role_inheritance() {
let mut ac = AccessControl::new();
let user = User::new("bob").with_role("editor");
ac.add_user(user);
let context = AccessContext::new();
assert!(ac.check_permission("bob", &Resource::Global, &Permission::Read, &context));
assert!(ac.check_permission("bob", &Resource::Global, &Permission::Write, &context));
}
#[test]
fn test_admin_role() {
let mut ac = AccessControl::new();
let user = User::new("admin").with_role("admin");
ac.add_user(user);
let context = AccessContext::new();
for permission in Permission::all() {
assert!(ac.check_permission("admin", &Resource::Global, &permission, &context));
}
}
#[test]
fn test_policy_allow() {
let mut ac = AccessControl::new();
let user = User::new("carol").with_role("viewer");
ac.add_user(user);
ac.add_policy(Policy {
id: "allow_write".to_string(),
subject: "carol".to_string(),
resource: Resource::Collection("col1".to_string()),
permission: Permission::Write,
effect: Effect::Allow,
conditions: Vec::new(),
});
let context = AccessContext::new();
assert!(!ac.check_permission("carol", &Resource::Global, &Permission::Write, &context));
}
#[test]
fn test_policy_deny() {
let mut ac = AccessControl::new();
let user = User::new("dave").with_role("editor");
ac.add_user(user);
ac.add_policy(Policy {
id: "deny_delete".to_string(),
subject: "dave".to_string(),
resource: Resource::Collection("restricted".to_string()),
permission: Permission::Delete,
effect: Effect::Deny,
conditions: Vec::new(),
});
let context = AccessContext::new();
assert!(!ac.check_permission(
"dave",
&Resource::Collection("restricted".to_string()),
&Permission::Delete,
&context
));
}
#[test]
fn test_condition_evaluation() {
let condition = Condition {
attribute: "ip_address".to_string(),
operator: Operator::StartsWith,
value: "192.168".to_string(),
};
let context = AccessContext::new().with_attribute("ip_address", "192.168.1.100");
assert!(condition.evaluate(&context));
let context2 = AccessContext::new().with_attribute("ip_address", "10.0.0.1");
assert!(!condition.evaluate(&context2));
}
#[test]
fn test_get_user_permissions() {
let mut ac = AccessControl::new();
let user = User::new("eve").with_role("editor");
ac.add_user(user);
let permissions = ac.get_user_permissions("eve", &Resource::Global);
assert!(!permissions.is_empty());
assert!(permissions.contains(&Permission::Read));
assert!(permissions.contains(&Permission::Write));
}
#[test]
fn test_unknown_user() {
let ac = AccessControl::new();
let context = AccessContext::new();
assert!(!ac.check_permission("unknown", &Resource::Global, &Permission::Read, &context));
}
}