use std::collections::HashMap;
use std::sync::Arc;
use parking_lot::RwLock;
use super::config::{Identity, RoleCondition, RoleMappingRule, RoleMappingCondition};
pub struct RoleMapper {
rules: Vec<RoleMappingRule>,
static_roles: Arc<RwLock<HashMap<String, Vec<String>>>>,
group_roles: HashMap<String, Vec<String>>,
default_roles: Vec<String>,
anonymous_role: Option<String>,
}
impl RoleMapper {
pub fn new() -> Self {
Self {
rules: Vec::new(),
static_roles: Arc::new(RwLock::new(HashMap::new())),
group_roles: HashMap::new(),
default_roles: Vec::new(),
anonymous_role: None,
}
}
pub fn builder() -> RoleMapperBuilder {
RoleMapperBuilder::new()
}
pub fn map_roles(&self, identity: &Identity) -> Vec<String> {
let mut roles = Vec::new();
roles.extend(identity.roles.clone());
if let Some(static_roles) = self.static_roles.read().get(&identity.user_id) {
roles.extend(static_roles.clone());
}
for group in &identity.groups {
if let Some(group_roles) = self.group_roles.get(group) {
roles.extend(group_roles.clone());
}
}
for rule in &self.rules {
if self.evaluate_rule(rule, identity) {
roles.extend(rule.assign_roles.clone());
}
}
if roles.is_empty() {
roles.extend(self.default_roles.clone());
}
roles.sort();
roles.dedup();
roles
}
pub fn map_primary_role(&self, identity: &Identity) -> Option<String> {
let roles = self.map_roles(identity);
roles.into_iter().next()
}
pub fn has_permission(&self, identity: &Identity, permission: &str) -> bool {
let roles = self.map_roles(identity);
for rule in &self.rules {
if roles.iter().any(|r| rule.assign_roles.contains(r)) {
if rule.permissions.contains(&permission.to_string()) {
return true;
}
}
}
false
}
pub fn get_permissions(&self, identity: &Identity) -> Vec<String> {
let roles = self.map_roles(identity);
let mut permissions = Vec::new();
for rule in &self.rules {
if roles.iter().any(|r| rule.assign_roles.contains(r)) {
permissions.extend(rule.permissions.clone());
}
}
permissions.sort();
permissions.dedup();
permissions
}
pub fn assign_static_role(&self, user_id: impl Into<String>, role: impl Into<String>) {
let user_id = user_id.into();
let role = role.into();
let mut static_roles = self.static_roles.write();
static_roles
.entry(user_id)
.or_insert_with(Vec::new)
.push(role);
}
pub fn remove_static_role(&self, user_id: &str, role: &str) {
let mut static_roles = self.static_roles.write();
if let Some(roles) = static_roles.get_mut(user_id) {
roles.retain(|r| r != role);
}
}
pub fn anonymous_role(&self) -> Option<&String> {
self.anonymous_role.as_ref()
}
fn evaluate_rule(&self, rule: &RoleMappingRule, identity: &Identity) -> bool {
for condition in &rule.conditions {
if !self.evaluate_condition(condition, identity) {
return false;
}
}
true
}
fn evaluate_condition(&self, condition: &RoleMappingCondition, identity: &Identity) -> bool {
match condition {
RoleMappingCondition::HasClaim { claim, value } => {
match identity.claims.get(claim) {
Some(claim_value) => {
if let Some(expected) = value {
claim_value.as_str() == Some(expected.as_str())
} else {
true }
}
None => false,
}
}
RoleMappingCondition::InGroup { group } => {
identity.groups.contains(group)
}
RoleMappingCondition::HasRole { role } => {
identity.roles.contains(role)
}
RoleMappingCondition::FromTenant { tenant_id } => {
identity.tenant_id.as_ref() == Some(tenant_id)
}
RoleMappingCondition::AuthMethod { method } => {
&identity.auth_method == method
}
RoleMappingCondition::EmailDomain { domain } => {
identity.email
.as_ref()
.map(|e| e.ends_with(&format!("@{}", domain)))
.unwrap_or(false)
}
RoleMappingCondition::UsernamePattern { pattern } => {
self.match_pattern(&identity.user_id, pattern)
}
RoleMappingCondition::And { conditions } => {
conditions.iter().all(|c| self.evaluate_condition(c, identity))
}
RoleMappingCondition::Or { conditions } => {
conditions.iter().any(|c| self.evaluate_condition(c, identity))
}
RoleMappingCondition::Not { condition } => {
!self.evaluate_condition(condition, identity)
}
}
}
fn match_pattern(&self, value: &str, pattern: &str) -> bool {
if pattern == "*" {
return true;
}
if let Some(prefix) = pattern.strip_suffix('*') {
return value.starts_with(prefix);
}
if let Some(suffix) = pattern.strip_prefix('*') {
return value.ends_with(suffix);
}
value == pattern
}
}
impl Default for RoleMapper {
fn default() -> Self {
Self::new()
}
}
pub struct RoleMapperBuilder {
rules: Vec<RoleMappingRule>,
group_roles: HashMap<String, Vec<String>>,
default_roles: Vec<String>,
anonymous_role: Option<String>,
}
impl RoleMapperBuilder {
pub fn new() -> Self {
Self {
rules: Vec::new(),
group_roles: HashMap::new(),
default_roles: Vec::new(),
anonymous_role: None,
}
}
pub fn rule(mut self, rule: RoleMappingRule) -> Self {
self.rules.push(rule);
self
}
pub fn group_role(mut self, group: impl Into<String>, role: impl Into<String>) -> Self {
let group = group.into();
let role = role.into();
self.group_roles
.entry(group)
.or_insert_with(Vec::new)
.push(role);
self
}
pub fn default_role(mut self, role: impl Into<String>) -> Self {
self.default_roles.push(role.into());
self
}
pub fn anonymous_role(mut self, role: impl Into<String>) -> Self {
self.anonymous_role = Some(role.into());
self
}
pub fn build(self) -> RoleMapper {
RoleMapper {
rules: self.rules,
static_roles: Arc::new(RwLock::new(HashMap::new())),
group_roles: self.group_roles,
default_roles: self.default_roles,
anonymous_role: self.anonymous_role,
}
}
}
impl Default for RoleMapperBuilder {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct PermissionSet {
pub databases: Vec<String>,
pub schemas: Vec<String>,
pub tables: Vec<String>,
pub operations: Vec<Operation>,
pub row_predicates: HashMap<String, String>,
pub column_restrictions: HashMap<String, Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Operation {
Select,
Insert,
Update,
Delete,
Create,
Drop,
Alter,
Grant,
Execute,
All,
}
impl Operation {
pub fn from_str(s: &str) -> Option<Self> {
match s.to_uppercase().as_str() {
"SELECT" => Some(Self::Select),
"INSERT" => Some(Self::Insert),
"UPDATE" => Some(Self::Update),
"DELETE" => Some(Self::Delete),
"CREATE" => Some(Self::Create),
"DROP" => Some(Self::Drop),
"ALTER" => Some(Self::Alter),
"GRANT" => Some(Self::Grant),
"EXECUTE" => Some(Self::Execute),
"ALL" => Some(Self::All),
_ => None,
}
}
}
impl PermissionSet {
pub fn empty() -> Self {
Self {
databases: Vec::new(),
schemas: Vec::new(),
tables: Vec::new(),
operations: Vec::new(),
row_predicates: HashMap::new(),
column_restrictions: HashMap::new(),
}
}
pub fn full_access() -> Self {
Self {
databases: vec!["*".to_string()],
schemas: vec!["*".to_string()],
tables: vec!["*".to_string()],
operations: vec![Operation::All],
row_predicates: HashMap::new(),
column_restrictions: HashMap::new(),
}
}
pub fn is_operation_allowed(&self, operation: &Operation, table: &str) -> bool {
if !self.operations.contains(&Operation::All) && !self.operations.contains(operation) {
return false;
}
if self.tables.is_empty() {
return true;
}
for pattern in &self.tables {
if pattern == "*" || pattern == table {
return true;
}
if pattern.ends_with('*') {
let prefix = &pattern[..pattern.len() - 1];
if table.starts_with(prefix) {
return true;
}
}
}
false
}
pub fn row_predicate(&self, table: &str) -> Option<&String> {
self.row_predicates.get(table)
}
pub fn allowed_columns(&self, table: &str) -> Option<&Vec<String>> {
self.column_restrictions.get(table)
}
}
#[derive(Debug, Clone)]
pub struct AuthorizationContext {
pub identity: Identity,
pub roles: Vec<String>,
pub permissions: PermissionSet,
pub session_start: chrono::DateTime<chrono::Utc>,
pub context: HashMap<String, String>,
}
impl AuthorizationContext {
pub fn new(identity: Identity, roles: Vec<String>, permissions: PermissionSet) -> Self {
Self {
identity,
roles,
permissions,
session_start: chrono::Utc::now(),
context: HashMap::new(),
}
}
pub fn is_allowed(&self, operation: &Operation, table: &str) -> bool {
self.permissions.is_operation_allowed(operation, table)
}
pub fn with_context(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.context.insert(key.into(), value.into());
self
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_identity() -> Identity {
Identity {
user_id: "user123".to_string(),
name: Some("Test User".to_string()),
email: Some("test@example.com".to_string()),
roles: vec!["user".to_string()],
groups: vec!["developers".to_string()],
tenant_id: Some("tenant1".to_string()),
claims: {
let mut claims = HashMap::new();
claims.insert("department".to_string(), serde_json::json!("engineering"));
claims
},
auth_method: "jwt".to_string(),
authenticated_at: chrono::Utc::now(),
}
}
#[test]
fn test_basic_role_mapping() {
let mapper = RoleMapper::builder()
.group_role("developers", "db_developer")
.default_role("db_user")
.build();
let identity = test_identity();
let roles = mapper.map_roles(&identity);
assert!(roles.contains(&"user".to_string())); assert!(roles.contains(&"db_developer".to_string())); }
#[test]
fn test_rule_based_mapping() {
let mapper = RoleMapper::builder()
.rule(RoleMappingRule {
name: "admin_from_claim".to_string(),
condition: RoleCondition::Always,
db_role: String::new(),
conditions: vec![
RoleMappingCondition::HasClaim {
claim: "department".to_string(),
value: Some("engineering".to_string()),
}
],
assign_roles: vec!["db_admin".to_string()],
permissions: vec!["read".to_string(), "write".to_string()],
priority: 1,
})
.build();
let identity = test_identity();
let roles = mapper.map_roles(&identity);
assert!(roles.contains(&"db_admin".to_string()));
}
#[test]
fn test_tenant_condition() {
let mapper = RoleMapper::builder()
.rule(RoleMappingRule {
name: "tenant1_role".to_string(),
condition: RoleCondition::Always,
db_role: String::new(),
conditions: vec![
RoleMappingCondition::FromTenant {
tenant_id: "tenant1".to_string(),
}
],
assign_roles: vec!["tenant1_user".to_string()],
permissions: Vec::new(),
priority: 1,
})
.build();
let identity = test_identity();
let roles = mapper.map_roles(&identity);
assert!(roles.contains(&"tenant1_user".to_string()));
}
#[test]
fn test_email_domain_condition() {
let mapper = RoleMapper::builder()
.rule(RoleMappingRule {
name: "example_domain".to_string(),
condition: RoleCondition::Always,
db_role: String::new(),
conditions: vec![
RoleMappingCondition::EmailDomain {
domain: "example.com".to_string(),
}
],
assign_roles: vec!["internal_user".to_string()],
permissions: Vec::new(),
priority: 1,
})
.build();
let identity = test_identity();
let roles = mapper.map_roles(&identity);
assert!(roles.contains(&"internal_user".to_string()));
}
#[test]
fn test_and_condition() {
let mapper = RoleMapper::builder()
.rule(RoleMappingRule {
name: "combined".to_string(),
condition: RoleCondition::Always,
db_role: String::new(),
conditions: vec![
RoleMappingCondition::And {
conditions: vec![
RoleMappingCondition::HasRole { role: "user".to_string() },
RoleMappingCondition::InGroup { group: "developers".to_string() },
],
}
],
assign_roles: vec!["power_user".to_string()],
permissions: Vec::new(),
priority: 1,
})
.build();
let identity = test_identity();
let roles = mapper.map_roles(&identity);
assert!(roles.contains(&"power_user".to_string()));
}
#[test]
fn test_or_condition() {
let mapper = RoleMapper::builder()
.rule(RoleMappingRule {
name: "either".to_string(),
condition: RoleCondition::Always,
db_role: String::new(),
conditions: vec![
RoleMappingCondition::Or {
conditions: vec![
RoleMappingCondition::HasRole { role: "admin".to_string() },
RoleMappingCondition::HasRole { role: "user".to_string() },
],
}
],
assign_roles: vec!["authenticated".to_string()],
permissions: Vec::new(),
priority: 1,
})
.build();
let identity = test_identity();
let roles = mapper.map_roles(&identity);
assert!(roles.contains(&"authenticated".to_string()));
}
#[test]
fn test_not_condition() {
let mapper = RoleMapper::builder()
.rule(RoleMappingRule {
name: "not_admin".to_string(),
condition: RoleCondition::Always,
db_role: String::new(),
conditions: vec![
RoleMappingCondition::Not {
condition: Box::new(RoleMappingCondition::HasRole {
role: "admin".to_string(),
}),
}
],
assign_roles: vec!["regular_user".to_string()],
permissions: Vec::new(),
priority: 1,
})
.build();
let identity = test_identity();
let roles = mapper.map_roles(&identity);
assert!(roles.contains(&"regular_user".to_string()));
}
#[test]
fn test_static_role_assignment() {
let mapper = RoleMapper::new();
mapper.assign_static_role("user123", "special_role");
let identity = test_identity();
let roles = mapper.map_roles(&identity);
assert!(roles.contains(&"special_role".to_string()));
}
#[test]
fn test_default_roles() {
let mapper = RoleMapper::builder()
.default_role("guest")
.build();
let identity = Identity {
user_id: "empty".to_string(),
name: None,
email: None,
roles: Vec::new(),
groups: Vec::new(),
tenant_id: None,
claims: HashMap::new(),
auth_method: "none".to_string(),
authenticated_at: chrono::Utc::now(),
};
let roles = mapper.map_roles(&identity);
assert!(roles.contains(&"guest".to_string()));
}
#[test]
fn test_permission_set() {
let permissions = PermissionSet {
databases: vec!["mydb".to_string()],
schemas: vec!["public".to_string()],
tables: vec!["users".to_string(), "orders*".to_string()],
operations: vec![Operation::Select, Operation::Insert],
row_predicates: {
let mut p = HashMap::new();
p.insert("users".to_string(), "id = current_user_id()".to_string());
p
},
column_restrictions: HashMap::new(),
};
assert!(permissions.is_operation_allowed(&Operation::Select, "users"));
assert!(permissions.is_operation_allowed(&Operation::Insert, "orders_2024"));
assert!(!permissions.is_operation_allowed(&Operation::Delete, "users"));
assert!(!permissions.is_operation_allowed(&Operation::Select, "secrets"));
assert_eq!(
permissions.row_predicate("users"),
Some(&"id = current_user_id()".to_string())
);
}
#[test]
fn test_pattern_matching() {
let mapper = RoleMapper::new();
assert!(mapper.match_pattern("admin_user", "admin*"));
assert!(mapper.match_pattern("user_admin", "*admin"));
assert!(mapper.match_pattern("anything", "*"));
assert!(mapper.match_pattern("exact", "exact"));
assert!(!mapper.match_pattern("mismatch", "exact"));
}
#[test]
fn test_authorization_context() {
let identity = test_identity();
let permissions = PermissionSet::full_access();
let roles = vec!["admin".to_string()];
let ctx = AuthorizationContext::new(identity, roles, permissions)
.with_context("client_ip", "192.168.1.1");
assert!(ctx.is_allowed(&Operation::Select, "any_table"));
assert_eq!(ctx.context.get("client_ip"), Some(&"192.168.1.1".to_string()));
}
}