myrmidon / python /src /server /services /rbac_service.py
tek Atrust
chore(deploy): build monolithic server for Hugging Face
d5ef46f
Raw
History Blame Contribute Delete
10.6 kB
# python/src/server/services/rbac_service.py
from ..auth.permissions import ROLE_PERMISSIONS
from ..config.logfire_config import get_logger
from ..utils import get_supabase_client
logger = get_logger(__name__)
class RBACService:
"""
Service for handling Role-Based Access Control (RBAC) logic.
Phase 4.6.31: Transitioned from hardcoded dicts to dynamic database-backed matrix.
"""
_matrix_cache: dict[str, set[str]] | None = None
def __init__(self):
# Initial assignment rules (Static fallback)
self.permissions = {
"admin": ["admin", "system_admin", "manager", "employee", "member", "marketing", "sales", "ai_agent"],
"system_admin": [
"admin",
"system_admin",
"manager",
"employee",
"member",
"marketing",
"sales",
"ai_agent",
],
"manager": ["manager", "employee", "member", "marketing", "sales", "ai_agent"],
"employee": ["employee", "member", "ai_agent"],
"member": ["employee", "member", "ai_agent"],
"marketing": ["marketing", "ai_agent"],
"sales": ["sales", "ai_agent"],
"pm": ["manager", "employee", "member", "marketing", "sales", "ai_agent"],
"engineer": ["employee", "member", "ai_agent"],
}
async def get_role_permissions(self, role: str) -> set[str]:
"""
Returns the set of permissions for a given role.
Priority: 1. DB (Dynamic) -> 2. permissions.py (Static Fallback)
"""
matrix = await self.get_matrix()
return matrix.get(role.lower(), set())
async def get_matrix(self, force_refresh: bool = False) -> dict[str, set[str]]:
"""Retrieves the full role-permission matrix, with caching support."""
if not force_refresh and self._matrix_cache is not None:
return self._matrix_cache
# Try to load from Database
try:
supabase = get_supabase_client()
result = supabase.table("archon_roles_permissions").select("role, permissions").execute()
if result.data:
dynamic_matrix = {item["role"].lower(): set(item["permissions"]) for item in result.data}
RBACService._matrix_cache = dynamic_matrix
logger.info(f"Loaded {len(dynamic_matrix)} roles from dynamic RBAC matrix")
return dynamic_matrix
except Exception as e:
logger.warning(f"Failed to load dynamic RBAC matrix from DB, falling back to static: {e}")
# Fallback to static permissions.py
static_matrix = {role.lower(): perms for role, perms in ROLE_PERMISSIONS.items()}
RBACService._matrix_cache = static_matrix
return static_matrix
def get_crawler_constraints(self, current_user_role: str | None) -> dict:
"""
Retrieves crawler constraints (max depth, concurrent) for a specific role
from archon_settings.
"""
role = (current_user_role or "member").lower()
# Mapping simple roles to their settings suffix
role_map = {
"admin": "ADMIN",
"system_admin": "ADMIN",
"manager": "MANAGER",
"marketing": "MARKETING",
"sales": "SALES",
}
suffix = role_map.get(role, "SALES") # Default to strictest (SALES) for unknown roles
# Default safe values if DB fetch fails
constraints = {
"max_depth": 2,
"max_concurrent": 3,
"allowed_domains": ["104.com.tw", "github.com", "google.com"],
}
try:
supabase = get_supabase_client()
# 1. Fetch static settings from archon_settings
keys = [f"CRAWL_MAX_DEPTH_{suffix}", f"CRAWL_CONCURRENT_MAX_{suffix}", "CRAWL_ALLOWED_DOMAINS_RESTRICTED"]
result = supabase.table("archon_settings").select("key, value").in_("key", keys).execute()
if result.data:
settings = {item["key"]: item["value"] for item in result.data}
depth_key = f"CRAWL_MAX_DEPTH_{suffix}"
if depth_key in settings:
constraints["max_depth"] = int(settings[depth_key])
concurrent_key = f"CRAWL_CONCURRENT_MAX_{suffix}"
if concurrent_key in settings:
constraints["max_concurrent"] = int(settings[concurrent_key])
if "CRAWL_ALLOWED_DOMAINS_RESTRICTED" in settings:
domains = settings["CRAWL_ALLOWED_DOMAINS_RESTRICTED"].split(",")
constraints["allowed_domains"] = [d.strip() for d in domains if d.strip()]
# 2. 物理加固:從 archon_crawler_targets 動態抓取 David 在 3737 設定的白名單
# 這實現了 David 在 3737 設定 URL 與後端爬蟲權限的物理連動
dynamic_result = (
supabase.table("archon_crawler_targets").select("whitelist, target_url").eq("is_active", True).execute()
)
if dynamic_result.data:
from typing import cast
allowed_domains = cast(list[str], constraints["allowed_domains"])
for target in dynamic_result.data:
# 加入主網址網域 (Domain) 作為基本許可
from urllib.parse import urlparse
domain = urlparse(target["target_url"]).netloc
if domain and domain not in allowed_domains:
allowed_domains.append(domain)
# 加入 David 設定的詳細白名單 Patterns
if target.get("whitelist"):
for pattern in target["whitelist"]:
if pattern and pattern not in allowed_domains:
allowed_domains.append(pattern)
except Exception as e:
logger.error(f"Failed to fetch crawler constraints for role {role}: {e}")
return constraints
async def has_permission_to_assign(self, current_user_role: str, assignee_role: str) -> bool:
"""Checks if the current user role has permission to assign tasks to the target role."""
if not current_user_role or not assignee_role:
return False
current_role = current_user_role.lower()
target_role = assignee_role.lower()
# 1. Try dynamic permission matrix first
perms = await self.get_role_permissions(current_role)
if perms:
if f"assign:{target_role}" in perms:
return True
if "assign:all" in perms or current_role in ["admin", "system_admin"]:
return True
# 2. Fallback to static permissions mapping
allowed_roles = self.permissions.get(current_role, [])
# Direct role match
if target_role in allowed_roles:
return True
# Agent wildcard check
if target_role == "ai_agent" and "ai_agent" in allowed_roles:
return True
return False
async def get_assignable_roles(self, current_user_role: str) -> list[str]:
"""Returns a list of roles that the current user can assign tasks to."""
if not current_user_role:
return []
current_role = current_user_role.lower()
# 1. Try dynamic permission matrix first
perms = await self.get_role_permissions(current_role)
if perms:
assignable = []
for p in perms:
if p.startswith("assign:"):
assignable.append(p.split(":")[1])
if assignable:
if "all" in assignable:
matrix = await self.get_matrix()
return list(matrix.keys())
return assignable
# 2. Fallback to static
return self.permissions.get(current_role, [])
def can_manage_content(self, current_user_role: str) -> bool:
"""Checks if the current user role has permission to manage content."""
if not current_user_role:
return False
# Define roles that can manage content (case-insensitive)
# Updated Phase 4.4: Sales and Marketing are content creators too.
content_manager_roles = ["admin", "system_admin", "manager", "marketing", "sales"]
return current_user_role.lower() in content_manager_roles
def scope_projects(self, projects: list[dict], user: dict) -> list[dict]:
"""
Filters a list of projects based on user's department and role.
Centralized logic from projects/core.py for Phase 4.6.30.
"""
role = user.get("role", "viewer").lower()
dept = user.get("department")
if role in ["system_admin", "admin"]:
return projects
return [p for p in projects if p.get("department") == dept or not p.get("department")]
def validate_project_access(self, project: dict, user: dict) -> bool:
"""
Validates if a user has access to a specific project based on department.
Centralized logic from projects/core.py for Phase 4.6.30.
"""
role = user.get("role", "viewer").lower()
dept = user.get("department")
if role in ["system_admin", "admin"]:
return True
project_dept = project.get("department")
if not project_dept:
return True
return bool(project_dept == dept)
async def get_restricted_mcp_tools(self, role_or_agent: str) -> set[str]:
"""
Returns a set of MCP tool names that are RESTRICTED (forbidden) for a given role or agent type.
Phase 5.1: Dynamic MCP Tool Schema Cropping based on RBAC.
"""
role = (role_or_agent or "anonymous").lower()
# High-level roles have no restrictions
if role in ["admin", "system_admin", "charlie"]:
return set()
# Base restrictions for all lower-level bots/users to protect infrastructure
restricted_tools = {"delete_project", "delete_task", "run_system_command", "execute_sql"}
# Additional restrictions based on specific roles
if role in ["marketbot", "marketing", "summary"]:
restricted_tools.update(["manage_project"])
elif role in ["librarian", "rag", "document"]:
restricted_tools.update(["manage_project", "manage_task"])
# Here we could extend to check DB permissions in the future
return restricted_tools