| |
|
|
| 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): |
| |
| 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: |
| 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}") |
|
|
| |
| 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() |
|
|
| |
| role_map = { |
| "admin": "ADMIN", |
| "system_admin": "ADMIN", |
| "manager": "MANAGER", |
| "marketing": "MARKETING", |
| "sales": "SALES", |
| } |
| suffix = role_map.get(role, "SALES") |
|
|
| |
| constraints = { |
| "max_depth": 2, |
| "max_concurrent": 3, |
| "allowed_domains": ["104.com.tw", "github.com", "google.com"], |
| } |
|
|
| try: |
| supabase = get_supabase_client() |
| |
| 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()] |
|
|
| |
| |
| 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: |
| |
| from urllib.parse import urlparse |
|
|
| domain = urlparse(target["target_url"]).netloc |
| if domain and domain not in allowed_domains: |
| allowed_domains.append(domain) |
|
|
| |
| 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() |
|
|
| |
| 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 |
|
|
| |
| allowed_roles = self.permissions.get(current_role, []) |
|
|
| |
| if target_role in allowed_roles: |
| return True |
|
|
| |
| 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() |
|
|
| |
| 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 |
|
|
| |
| 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 |
| |
| |
| 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() |
|
|
| |
| if role in ["admin", "system_admin", "charlie"]: |
| return set() |
|
|
| |
| restricted_tools = {"delete_project", "delete_task", "run_system_command", "execute_sql"} |
|
|
| |
| if role in ["marketbot", "marketing", "summary"]: |
| restricted_tools.update(["manage_project"]) |
| elif role in ["librarian", "rag", "document"]: |
| restricted_tools.update(["manage_project", "manage_task"]) |
|
|
| |
| return restricted_tools |
|
|