/*****************************************************************************\ | █████ █████ ██ █ █████ █████ ████ ██ ████ █████ █████ ███ ® | | ██ █ ███ █ █ ██ ██ ██ ██ ██ ██ █ ██ ██ █ █ | | ██ ███ ████ █ ██ █ ████ █████ ██████ ██ ████ █ █ █ ██ | | ██ ██ █ █ ██ █ █ ██ ██ ██ ██ ██ ██ █ ██ ██ █ █ | | █████ █████ █ ███ █████ ██ ██ ██ ██ █████ ████ █████ █ ███ | | | | General Bots Copyright (c) pragmatismo.com.br. All rights reserved. | | Licensed under the AGPL-3.0. | | | | According to our dual licensing model, this program can be used either | | under the terms of the GNU Affero General Public License, version 3, | | or under a proprietary license. | | | | The texts of the GNU Affero General Public License with an additional | | permission and of our proprietary license can be found at and | | in the LICENSE file you have received along with this program. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY, without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | "General Bots" is a registered trademark of pragmatismo.com.br. | | The licensing of the program under the AGPLv3 does not imply a | | trademark license. Therefore any rights, title and interest in | | our trademarks remain entirely with us. | | | \*****************************************************************************/ use crate::shared::models::UserSession; use diesel::prelude::*; use diesel::sql_query; use diesel::sql_types::Text; use log::{trace, warn}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct UserRoles { pub roles: Vec, pub user_id: Option, } impl UserRoles { pub fn new(roles: Vec) -> Self { Self { roles: roles.into_iter().map(|r| r.to_lowercase()).collect(), user_id: None, } } pub fn with_user_id(roles: Vec, user_id: uuid::Uuid) -> Self { Self { roles: roles.into_iter().map(|r| r.to_lowercase()).collect(), user_id: Some(user_id), } } pub fn anonymous() -> Self { Self::default() } pub fn from_user_session(session: &UserSession) -> Self { let mut roles = Vec::new(); // Try different keys where roles might be stored let role_keys = ["roles", "user_roles", "zitadel_roles"]; for key in role_keys { if let Some(value) = session.context_data.get(key) { match value { // Array of strings Value::Array(arr) => { for item in arr { if let Value::String(s) = item { roles.push(s.trim().to_lowercase()); } } if !roles.is_empty() { break; } } // Semicolon-separated string Value::String(s) => { roles = s .split(';') .map(|r| r.trim().to_lowercase()) .filter(|r| !r.is_empty()) .collect(); if !roles.is_empty() { break; } } _ => {} } } } // Also check if user is marked as admin in context if let Some(Value::Bool(true)) = session.context_data.get("is_admin") { if !roles.contains(&"admin".to_string()) { roles.push("admin".to_string()); } } Self { roles, user_id: Some(session.user_id), } } pub fn has_access(&self, required_roles: &[String]) -> bool { if required_roles.is_empty() { return true; // No roles specified = everyone has access } // Check if user has any of the required roles self.roles.iter().any(|user_role| { required_roles .iter() .any(|req| req.to_lowercase() == *user_role) }) } pub fn has_role(&self, role: &str) -> bool { self.roles.iter().any(|r| r == &role.to_lowercase()) } pub fn is_admin(&self) -> bool { self.has_role("admin") } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AccessType { Read, Write, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct TableAccessInfo { pub table_name: String, pub read_roles: Vec, pub write_roles: Vec, pub field_read_roles: HashMap>, pub field_write_roles: HashMap>, } impl TableAccessInfo { pub fn can_read(&self, user_roles: &UserRoles) -> bool { user_roles.has_access(&self.read_roles) } pub fn can_write(&self, user_roles: &UserRoles) -> bool { user_roles.has_access(&self.write_roles) } pub fn can_read_field(&self, field_name: &str, user_roles: &UserRoles) -> bool { if let Some(field_roles) = self.field_read_roles.get(field_name) { user_roles.has_access(field_roles) } else { true // No field-level restriction } } pub fn can_write_field(&self, field_name: &str, user_roles: &UserRoles) -> bool { if let Some(field_roles) = self.field_write_roles.get(field_name) { user_roles.has_access(field_roles) } else { true // No field-level restriction } } pub fn get_restricted_read_fields(&self, user_roles: &UserRoles) -> Vec { self.field_read_roles .iter() .filter(|(_, roles)| !user_roles.has_access(roles)) .map(|(field, _)| field.clone()) .collect() } pub fn get_restricted_write_fields(&self, user_roles: &UserRoles) -> Vec { self.field_write_roles .iter() .filter(|(_, roles)| !user_roles.has_access(roles)) .map(|(field, _)| field.clone()) .collect() } } #[derive(QueryableByName, Debug)] struct TableDefRow { #[diesel(sql_type = Text)] table_name: String, #[diesel(sql_type = diesel::sql_types::Nullable)] read_roles: Option, #[diesel(sql_type = diesel::sql_types::Nullable)] write_roles: Option, } #[derive(QueryableByName, Debug)] struct FieldDefRow { #[diesel(sql_type = Text)] field_name: String, #[diesel(sql_type = diesel::sql_types::Nullable)] read_roles: Option, #[diesel(sql_type = diesel::sql_types::Nullable)] write_roles: Option, } pub fn load_table_access_info( conn: &mut diesel::PgConnection, table_name: &str, ) -> Option { // Query table-level permissions let table_result: Result = sql_query( "SELECT table_name, read_roles, write_roles FROM dynamic_table_definitions WHERE table_name = $1 LIMIT 1", ) .bind::(table_name) .get_result(conn); let table_def = match table_result { Ok(row) => row, Err(_) => { trace!( "No table definition found for '{}', allowing open access", table_name ); return None; // No definition = open access } }; let mut info = TableAccessInfo { table_name: table_def.table_name, read_roles: parse_roles_string(&table_def.read_roles), write_roles: parse_roles_string(&table_def.write_roles), field_read_roles: HashMap::new(), field_write_roles: HashMap::new(), }; // Query field-level permissions let fields_result: Result, _> = sql_query( "SELECT f.field_name, f.read_roles, f.write_roles FROM dynamic_table_fields f JOIN dynamic_table_definitions t ON f.table_definition_id = t.id WHERE t.table_name = $1", ) .bind::(table_name) .get_results(conn); if let Ok(fields) = fields_result { for field in fields { let field_read = parse_roles_string(&field.read_roles); let field_write = parse_roles_string(&field.write_roles); if !field_read.is_empty() { info.field_read_roles .insert(field.field_name.clone(), field_read); } if !field_write.is_empty() { info.field_write_roles.insert(field.field_name, field_write); } } } trace!( "Loaded access info for table '{}': read_roles={:?}, write_roles={:?}, field_restrictions={}", info.table_name, info.read_roles, info.write_roles, info.field_read_roles.len() + info.field_write_roles.len() ); Some(info) } fn parse_roles_string(roles: &Option) -> Vec { roles .as_ref() .map(|s| { s.split(';') .map(|r| r.trim().to_string()) .filter(|r| !r.is_empty()) .collect() }) .unwrap_or_default() } pub fn check_table_access( conn: &mut diesel::PgConnection, table_name: &str, user_roles: &UserRoles, access_type: AccessType, ) -> Result, String> { let access_info = load_table_access_info(conn, table_name); if let Some(ref info) = access_info { let has_access = match access_type { AccessType::Read => info.can_read(user_roles), AccessType::Write => info.can_write(user_roles), }; if !has_access { let action = match access_type { AccessType::Read => "read from", AccessType::Write => "write to", }; warn!( "Access denied: user {:?} cannot {} table '{}'", user_roles.user_id, action, table_name ); return Err(format!( "Access denied: insufficient permissions to {} table '{}'", action, table_name )); } } Ok(access_info) } pub fn check_field_write_access( fields: &[String], user_roles: &UserRoles, access_info: &Option, ) -> Result<(), String> { let Some(info) = access_info else { return Ok(()); // No access info = allow all }; let mut denied_fields = Vec::new(); for field in fields { if !info.can_write_field(field, user_roles) { denied_fields.push(field.clone()); } } if denied_fields.is_empty() { Ok(()) } else { Err(format!( "Access denied: insufficient permissions to write field(s): {}", denied_fields.join(", ") )) } } pub fn filter_fields_by_role( data: Value, user_roles: &UserRoles, access_info: &Option, ) -> Value { let Some(info) = access_info else { return data; // No access info = return all fields }; match data { Value::Object(mut map) => { let restricted = info.get_restricted_read_fields(user_roles); for field in restricted { trace!("Filtering out field '{}' due to role restriction", field); map.remove(&field); } Value::Object(map) } Value::Array(arr) => Value::Array( arr.into_iter() .map(|v| filter_fields_by_role(v, user_roles, access_info)) .collect(), ), other => other, } } pub fn filter_write_fields( data: Value, user_roles: &UserRoles, access_info: &Option, ) -> (Value, Vec) { let Some(info) = access_info else { return (data, Vec::new()); // No access info = allow all }; match data { Value::Object(mut map) => { let restricted = info.get_restricted_write_fields(user_roles); let mut removed = Vec::new(); for field in &restricted { if map.contains_key(field) { trace!( "Removing field '{}' from write data due to role restriction", field ); map.remove(field); removed.push(field.clone()); } } (Value::Object(map), removed) } other => (other, Vec::new()), } } #[cfg(test)] mod tests { use super::*; #[test] fn test_user_roles_has_access() { let roles = UserRoles::new(vec!["admin".to_string(), "manager".to_string()]); // Empty roles = everyone allowed assert!(roles.has_access(&[])); // User has admin role assert!(roles.has_access(&["admin".to_string()])); // User has manager role assert!(roles.has_access(&["manager".to_string(), "superuser".to_string()])); // User doesn't have superuser role only assert!(!roles.has_access(&["superuser".to_string()])); } #[test] fn test_user_roles_case_insensitive() { let roles = UserRoles::new(vec!["Admin".to_string()]); assert!(roles.has_access(&["admin".to_string()])); assert!(roles.has_access(&["ADMIN".to_string()])); assert!(roles.has_access(&["Admin".to_string()])); } #[test] fn test_anonymous_user() { let roles = UserRoles::anonymous(); // Anonymous can access if no roles required assert!(roles.has_access(&[])); // Anonymous cannot access if roles required assert!(!roles.has_access(&["admin".to_string()])); } #[test] fn test_table_access_info_field_restrictions() { let mut info = TableAccessInfo { table_name: "contacts".to_string(), read_roles: vec![], write_roles: vec![], field_read_roles: HashMap::new(), field_write_roles: HashMap::new(), }; info.field_read_roles .insert("ssn".to_string(), vec!["admin".to_string()]); info.field_write_roles .insert("salary".to_string(), vec!["hr".to_string()]); let admin = UserRoles::new(vec!["admin".to_string()]); let hr = UserRoles::new(vec!["hr".to_string()]); let user = UserRoles::new(vec!["user".to_string()]); // Admin can read SSN assert!(info.can_read_field("ssn", &admin)); // Regular user cannot read SSN assert!(!info.can_read_field("ssn", &user)); // HR can write salary assert!(info.can_write_field("salary", &hr)); // Admin cannot write salary (different role) assert!(!info.can_write_field("salary", &admin)); // Everyone can read/write unrestricted fields assert!(info.can_read_field("name", &user)); assert!(info.can_write_field("name", &user)); } #[test] fn test_filter_fields_by_role() { let mut info = TableAccessInfo::default(); info.field_read_roles .insert("secret".to_string(), vec!["admin".to_string()]); let data = serde_json::json!({ "id": 1, "name": "John", "secret": "classified" }); let user = UserRoles::new(vec!["user".to_string()]); let filtered = filter_fields_by_role(data.clone(), &user, &Some(info.clone())); assert!(filtered.get("id").is_some()); assert!(filtered.get("name").is_some()); assert!(filtered.get("secret").is_none()); // Admin can see everything let admin = UserRoles::new(vec!["admin".to_string()]); let not_filtered = filter_fields_by_role(data, &admin, &Some(info)); assert!(not_filtered.get("secret").is_some()); } #[test] fn test_parse_roles_string() { assert_eq!(parse_roles_string(&None), Vec::::new()); assert_eq!( parse_roles_string(&Some("".to_string())), Vec::::new() ); assert_eq!( parse_roles_string(&Some("admin".to_string())), vec!["admin"] ); assert_eq!( parse_roles_string(&Some("admin;manager".to_string())), vec!["admin", "manager"] ); assert_eq!( parse_roles_string(&Some(" admin ; manager ; hr ".to_string())), vec!["admin", "manager", "hr"] ); } }