use std::str::FromStr;
use crate::types::TenantId;
#[derive(Debug, Clone)]
pub struct AuthenticatedIdentity {
pub user_id: u64,
pub username: String,
pub tenant_id: TenantId,
pub auth_method: AuthMethod,
pub roles: Vec<Role>,
pub is_superuser: bool,
}
impl AuthenticatedIdentity {
pub fn has_role(&self, role: &Role) -> bool {
self.is_superuser || self.roles.contains(role)
}
pub fn has_any_role(&self, roles: &[Role]) -> bool {
self.is_superuser || roles.iter().any(|r| self.roles.contains(r))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthMethod {
ScramSha256,
CleartextPassword,
ApiKey,
Certificate,
Trust,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Role {
Superuser,
TenantAdmin,
ReadWrite,
ReadOnly,
Monitor,
Custom(String),
}
impl std::fmt::Display for Role {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Role::Superuser => write!(f, "superuser"),
Role::TenantAdmin => write!(f, "tenant_admin"),
Role::ReadWrite => write!(f, "readwrite"),
Role::ReadOnly => write!(f, "readonly"),
Role::Monitor => write!(f, "monitor"),
Role::Custom(name) => write!(f, "{name}"),
}
}
}
impl FromStr for Role {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
Ok(match s {
"superuser" => Role::Superuser,
"tenant_admin" => Role::TenantAdmin,
"readwrite" => Role::ReadWrite,
"readonly" => Role::ReadOnly,
"monitor" => Role::Monitor,
other => Role::Custom(other.to_string()),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Permission {
Read,
Write,
Create,
Drop,
Alter,
Admin,
Monitor,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum PermissionTarget {
Cluster,
Tenant(TenantId),
Collection {
tenant_id: TenantId,
collection: String,
},
SystemCatalog,
}
pub fn role_grants_permission(role: &Role, permission: Permission) -> bool {
match role {
Role::Superuser => true,
Role::TenantAdmin => true,
Role::ReadWrite => matches!(permission, Permission::Read | Permission::Write),
Role::ReadOnly => matches!(permission, Permission::Read),
Role::Monitor => matches!(permission, Permission::Monitor | Permission::Read),
Role::Custom(_) => false, }
}
pub fn required_permission(plan: &crate::bridge::envelope::PhysicalPlan) -> Permission {
use crate::bridge::envelope::PhysicalPlan;
use crate::bridge::physical_plan::{
CrdtOp, DocumentOp, GraphOp, KvOp, MetaOp, QueryOp, SpatialOp, TextOp, TimeseriesOp,
VectorOp,
};
match plan {
PhysicalPlan::Document(
DocumentOp::PointGet { .. }
| DocumentOp::RangeScan { .. }
| DocumentOp::Scan { .. }
| DocumentOp::IndexLookup { .. }
| DocumentOp::EstimateCount { .. },
) => Permission::Read,
PhysicalPlan::Vector(VectorOp::Search { .. } | VectorOp::MultiSearch { .. }) => {
Permission::Read
}
PhysicalPlan::Crdt(CrdtOp::Read { .. }) => Permission::Read,
PhysicalPlan::Graph(
GraphOp::Hop { .. }
| GraphOp::Neighbors { .. }
| GraphOp::Path { .. }
| GraphOp::Subgraph { .. }
| GraphOp::RagFusion { .. }
| GraphOp::Algo { .. }
| GraphOp::Match { .. },
) => Permission::Read,
PhysicalPlan::Query(
QueryOp::Aggregate { .. }
| QueryOp::HashJoin { .. }
| QueryOp::PartialAggregate { .. }
| QueryOp::BroadcastJoin { .. }
| QueryOp::ShuffleJoin { .. }
| QueryOp::NestedLoopJoin { .. },
) => Permission::Read,
PhysicalPlan::Text(TextOp::Search { .. } | TextOp::HybridSearch { .. }) => Permission::Read,
PhysicalPlan::Spatial(SpatialOp::Scan { .. }) => Permission::Read,
PhysicalPlan::Timeseries(TimeseriesOp::Scan { .. }) => Permission::Read,
PhysicalPlan::Crdt(CrdtOp::Apply { .. }) => Permission::Write,
PhysicalPlan::Vector(
VectorOp::Insert { .. } | VectorOp::BatchInsert { .. } | VectorOp::Delete { .. },
) => Permission::Write,
PhysicalPlan::Document(
DocumentOp::BatchInsert { .. }
| DocumentOp::PointPut { .. }
| DocumentOp::PointDelete { .. }
| DocumentOp::PointUpdate { .. }
| DocumentOp::BulkUpdate { .. }
| DocumentOp::BulkDelete { .. }
| DocumentOp::Upsert { .. }
| DocumentOp::InsertSelect { .. }
| DocumentOp::Truncate { .. },
) => Permission::Write,
PhysicalPlan::Graph(GraphOp::EdgePut { .. } | GraphOp::EdgeDelete { .. }) => {
Permission::Write
}
PhysicalPlan::Meta(MetaOp::WalAppend { .. }) => Permission::Write,
PhysicalPlan::Timeseries(TimeseriesOp::Ingest { .. }) => Permission::Write,
PhysicalPlan::Meta(MetaOp::TransactionBatch { .. }) => Permission::Write,
PhysicalPlan::Document(DocumentOp::Register { .. } | DocumentOp::DropIndex { .. }) => {
Permission::Alter
}
PhysicalPlan::Crdt(CrdtOp::SetPolicy { .. }) => Permission::Alter,
PhysicalPlan::Vector(VectorOp::SetParams { .. }) => Permission::Alter,
PhysicalPlan::Meta(MetaOp::Cancel { .. }) => Permission::Admin,
PhysicalPlan::Meta(MetaOp::CreateSnapshot | MetaOp::Compact | MetaOp::Checkpoint) => {
Permission::Admin
}
PhysicalPlan::Kv(
KvOp::Get { .. } | KvOp::Scan { .. } | KvOp::BatchGet { .. } | KvOp::FieldGet { .. },
) => Permission::Read,
PhysicalPlan::Kv(
KvOp::Put { .. }
| KvOp::Delete { .. }
| KvOp::Expire { .. }
| KvOp::Persist { .. }
| KvOp::BatchPut { .. }
| KvOp::RegisterIndex { .. }
| KvOp::DropIndex { .. }
| KvOp::FieldSet { .. }
| KvOp::Truncate { .. },
) => Permission::Write,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_identity(roles: Vec<Role>, superuser: bool) -> AuthenticatedIdentity {
AuthenticatedIdentity {
user_id: 1,
username: "test".into(),
tenant_id: TenantId::new(1),
auth_method: AuthMethod::Trust,
roles,
is_superuser: superuser,
}
}
#[test]
fn superuser_has_all_roles() {
let id = test_identity(vec![], true);
assert!(id.has_role(&Role::ReadOnly));
assert!(id.has_role(&Role::TenantAdmin));
assert!(id.has_role(&Role::Custom("anything".into())));
}
#[test]
fn readonly_only_has_readonly() {
let id = test_identity(vec![Role::ReadOnly], false);
assert!(id.has_role(&Role::ReadOnly));
assert!(!id.has_role(&Role::ReadWrite));
assert!(!id.has_role(&Role::TenantAdmin));
}
#[test]
fn role_permission_mapping() {
assert!(role_grants_permission(&Role::ReadOnly, Permission::Read));
assert!(!role_grants_permission(&Role::ReadOnly, Permission::Write));
assert!(role_grants_permission(&Role::ReadWrite, Permission::Read));
assert!(role_grants_permission(&Role::ReadWrite, Permission::Write));
assert!(!role_grants_permission(&Role::ReadWrite, Permission::Drop));
assert!(role_grants_permission(
&Role::TenantAdmin,
Permission::Admin
));
assert!(role_grants_permission(&Role::TenantAdmin, Permission::Drop));
}
#[test]
fn role_display_roundtrip() {
let roles = [
Role::Superuser,
Role::TenantAdmin,
Role::ReadWrite,
Role::ReadOnly,
Role::Monitor,
];
for role in &roles {
let s = role.to_string();
let parsed: Role = s.parse().unwrap();
assert_eq!(*role, parsed);
}
}
}