Initial commit
This commit is contained in:
17
app/core/permissions/__init__.py
Normal file
17
app/core/permissions/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
Permission management module.
|
||||
|
||||
This module provides a unified permission service for managing access control
|
||||
across the application.
|
||||
"""
|
||||
|
||||
from app.core.permissions.models import Action, ResourceType, Resource, Subject
|
||||
from app.core.permissions.service import permission_service
|
||||
|
||||
__all__ = [
|
||||
"Action",
|
||||
"ResourceType",
|
||||
"Resource",
|
||||
"Subject",
|
||||
"permission_service",
|
||||
]
|
||||
133
app/core/permissions/models.py
Normal file
133
app/core/permissions/models.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""
|
||||
Permission models for access control.
|
||||
|
||||
Defines the core models used in the permission system:
|
||||
- Action: Types of operations that can be performed
|
||||
- ResourceType: Types of resources in the system
|
||||
- Resource: Represents a resource with ownership and tenant information
|
||||
- Subject: Represents a user/actor performing an action
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
from typing import Set, Optional
|
||||
from dataclasses import dataclass, field
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
class Action(Enum):
|
||||
"""Operation types that can be performed on resources."""
|
||||
CREATE = "create"
|
||||
READ = "read"
|
||||
UPDATE = "update"
|
||||
DELETE = "delete"
|
||||
SHARE = "share"
|
||||
MANAGE = "manage"
|
||||
ACTIVATE = "activate"
|
||||
DEACTIVATE = "deactivate"
|
||||
|
||||
|
||||
class ResourceType(Enum):
|
||||
"""Types of resources in the system."""
|
||||
FILE = "file"
|
||||
WORKSPACE = "workspace"
|
||||
KNOWLEDGE = "knowledge"
|
||||
APP = "app"
|
||||
USER = "user"
|
||||
DOCUMENT = "document"
|
||||
MODEL = "model"
|
||||
CHUNK = "chunk"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Resource:
|
||||
"""
|
||||
Represents a resource in the system.
|
||||
|
||||
Attributes:
|
||||
type: The type of resource
|
||||
id: Unique identifier of the resource
|
||||
owner_id: ID of the user who owns the resource
|
||||
tenant_id: ID of the tenant the resource belongs to
|
||||
is_public: Whether the resource is publicly accessible within the tenant
|
||||
metadata: Additional resource-specific metadata
|
||||
"""
|
||||
type: ResourceType
|
||||
id: UUID
|
||||
owner_id: UUID
|
||||
tenant_id: UUID
|
||||
is_public: bool = False
|
||||
metadata: dict = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, file_obj) -> "Resource":
|
||||
"""Create a Resource from a GenericFile model instance."""
|
||||
return cls(
|
||||
type=ResourceType.FILE,
|
||||
id=file_obj.id,
|
||||
owner_id=file_obj.created_by,
|
||||
tenant_id=file_obj.tenant_id,
|
||||
is_public=getattr(file_obj, 'is_public', False),
|
||||
metadata={
|
||||
"file_name": file_obj.file_name,
|
||||
"context": file_obj.context,
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_workspace(cls, workspace_obj) -> "Resource":
|
||||
"""Create a Resource from a Workspace model instance."""
|
||||
return cls(
|
||||
type=ResourceType.WORKSPACE,
|
||||
id=workspace_obj.id,
|
||||
owner_id=workspace_obj.tenant_id,
|
||||
tenant_id=workspace_obj.tenant_id,
|
||||
is_public=False,
|
||||
metadata={
|
||||
"name": workspace_obj.name,
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_user(cls, user_obj) -> "Resource":
|
||||
"""Create a Resource from a User model instance."""
|
||||
return cls(
|
||||
type=ResourceType.USER,
|
||||
id=user_obj.id,
|
||||
owner_id=user_obj.id, # User owns themselves
|
||||
tenant_id=user_obj.tenant_id,
|
||||
is_public=False,
|
||||
metadata={
|
||||
"username": user_obj.username,
|
||||
"is_superuser": user_obj.is_superuser,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Subject:
|
||||
"""
|
||||
Represents a user/actor performing an action.
|
||||
|
||||
Attributes:
|
||||
id: User ID
|
||||
tenant_id: Tenant ID the user belongs to
|
||||
is_superuser: Whether the user is a superuser
|
||||
roles: Set of role names the user has
|
||||
workspace_memberships: Set of workspace IDs the user is a member of
|
||||
"""
|
||||
id: UUID
|
||||
tenant_id: UUID
|
||||
is_superuser: bool = False
|
||||
roles: Set[str] = field(default_factory=set)
|
||||
workspace_memberships: Set[UUID] = field(default_factory=set)
|
||||
|
||||
@classmethod
|
||||
def from_user(cls, user_obj, workspace_memberships: Optional[Set[UUID]] = None) -> "Subject":
|
||||
"""Create a Subject from a User model instance."""
|
||||
return cls(
|
||||
id=user_obj.id,
|
||||
tenant_id=user_obj.tenant_id,
|
||||
is_superuser=user_obj.is_superuser,
|
||||
roles=set(getattr(user_obj, 'roles', [])),
|
||||
workspace_memberships=workspace_memberships or set()
|
||||
)
|
||||
151
app/core/permissions/policies.py
Normal file
151
app/core/permissions/policies.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""
|
||||
Permission policies for access control.
|
||||
|
||||
Defines various policy classes that implement different permission rules:
|
||||
- SuperuserPolicy: Superusers can perform any action
|
||||
- OwnerPolicy: Resource owners can perform any action on their resources
|
||||
- TenantPolicy: Users in the same tenant can access public resources
|
||||
- RoleBasedPolicy: Permission based on user roles
|
||||
- WorkspaceMemberPolicy: Workspace members can access workspace resources
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Set
|
||||
from app.core.permissions.models import Subject, Resource, Action, ResourceType
|
||||
|
||||
|
||||
class PermissionPolicy(ABC):
|
||||
"""Base class for permission policies."""
|
||||
|
||||
@abstractmethod
|
||||
def can_perform(self, subject: Subject, action: Action, resource: Resource) -> bool:
|
||||
"""
|
||||
Determine if a subject can perform an action on a resource.
|
||||
|
||||
Args:
|
||||
subject: The user/actor attempting the action
|
||||
action: The action being attempted
|
||||
resource: The resource being acted upon
|
||||
|
||||
Returns:
|
||||
True if the action is allowed, False otherwise
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class SuperuserPolicy(PermissionPolicy):
|
||||
"""Superusers can perform any action on any resource."""
|
||||
|
||||
def can_perform(self, subject: Subject, action: Action, resource: Resource) -> bool:
|
||||
return subject.is_superuser
|
||||
|
||||
|
||||
class OwnerPolicy(PermissionPolicy):
|
||||
"""Resource owners can perform any action on their own resources."""
|
||||
|
||||
def can_perform(self, subject: Subject, action: Action, resource: Resource) -> bool:
|
||||
return subject.id == resource.owner_id
|
||||
|
||||
|
||||
class TenantPolicy(PermissionPolicy):
|
||||
"""
|
||||
Users in the same tenant can access public resources.
|
||||
|
||||
Args:
|
||||
allowed_actions: Set of actions allowed on public resources (default: READ only)
|
||||
"""
|
||||
|
||||
def __init__(self, allowed_actions: Set[Action] = None):
|
||||
self.allowed_actions = allowed_actions or {Action.READ}
|
||||
|
||||
def can_perform(self, subject: Subject, action: Action, resource: Resource) -> bool:
|
||||
return (
|
||||
subject.tenant_id == resource.tenant_id and
|
||||
resource.is_public and
|
||||
action in self.allowed_actions
|
||||
)
|
||||
|
||||
|
||||
class RoleBasedPolicy(PermissionPolicy):
|
||||
"""
|
||||
Permission based on user roles.
|
||||
|
||||
Args:
|
||||
required_roles: Set of roles that grant permission
|
||||
allowed_actions: Set of actions these roles can perform
|
||||
"""
|
||||
|
||||
def __init__(self, required_roles: Set[str], allowed_actions: Set[Action]):
|
||||
self.required_roles = required_roles
|
||||
self.allowed_actions = allowed_actions
|
||||
|
||||
def can_perform(self, subject: Subject, action: Action, resource: Resource) -> bool:
|
||||
has_role = bool(subject.roles & self.required_roles)
|
||||
return has_role and action in self.allowed_actions
|
||||
|
||||
|
||||
class WorkspaceMemberPolicy(PermissionPolicy):
|
||||
"""
|
||||
Workspace members can access workspace resources.
|
||||
|
||||
Args:
|
||||
allowed_actions: Set of actions workspace members can perform
|
||||
"""
|
||||
|
||||
def __init__(self, allowed_actions: Set[Action] = None):
|
||||
self.allowed_actions = allowed_actions or {Action.READ, Action.UPDATE}
|
||||
|
||||
def can_perform(self, subject: Subject, action: Action, resource: Resource) -> bool:
|
||||
if resource.type != ResourceType.WORKSPACE:
|
||||
return False
|
||||
|
||||
return (
|
||||
resource.id in subject.workspace_memberships and
|
||||
action in self.allowed_actions
|
||||
)
|
||||
|
||||
|
||||
class SameTenantSuperuserPolicy(PermissionPolicy):
|
||||
"""
|
||||
Superusers in the same tenant can perform specific actions.
|
||||
|
||||
This is useful for tenant-scoped admin operations where even superusers
|
||||
should be limited to their own tenant.
|
||||
|
||||
Args:
|
||||
allowed_actions: Set of actions allowed (default: all actions)
|
||||
"""
|
||||
|
||||
def __init__(self, allowed_actions: Set[Action] = None):
|
||||
self.allowed_actions = allowed_actions or set(Action)
|
||||
|
||||
def can_perform(self, subject: Subject, action: Action, resource: Resource) -> bool:
|
||||
return (
|
||||
subject.is_superuser and
|
||||
subject.tenant_id == resource.tenant_id and
|
||||
action in self.allowed_actions
|
||||
)
|
||||
|
||||
|
||||
class SelfAccessPolicy(PermissionPolicy):
|
||||
"""
|
||||
Users can access their own user resource.
|
||||
|
||||
This is specifically for user resources where users should be able
|
||||
to read/update their own profile.
|
||||
|
||||
Args:
|
||||
allowed_actions: Set of actions users can perform on themselves
|
||||
"""
|
||||
|
||||
def __init__(self, allowed_actions: Set[Action] = None):
|
||||
self.allowed_actions = allowed_actions or {Action.READ, Action.UPDATE}
|
||||
|
||||
def can_perform(self, subject: Subject, action: Action, resource: Resource) -> bool:
|
||||
if resource.type != ResourceType.USER:
|
||||
return False
|
||||
|
||||
return (
|
||||
subject.id == resource.id and
|
||||
action in self.allowed_actions
|
||||
)
|
||||
176
app/core/permissions/service.py
Normal file
176
app/core/permissions/service.py
Normal file
@@ -0,0 +1,176 @@
|
||||
"""
|
||||
Unified permission service for centralized access control.
|
||||
|
||||
This service provides a single point for all permission checks in the application,
|
||||
replacing scattered inline permission logic.
|
||||
"""
|
||||
|
||||
from typing import List, Optional
|
||||
from app.core.permissions.models import Subject, Resource, Action
|
||||
from app.core.permissions.policies import (
|
||||
PermissionPolicy,
|
||||
SuperuserPolicy,
|
||||
OwnerPolicy,
|
||||
TenantPolicy,
|
||||
SelfAccessPolicy,
|
||||
)
|
||||
from app.core.exceptions import PermissionDeniedException
|
||||
from app.core.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class PermissionService:
|
||||
"""
|
||||
Centralized permission service.
|
||||
|
||||
Uses a chain of permission policies to determine if an action is allowed.
|
||||
Any policy in the chain can grant permission (OR logic).
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# Default policy chain - order matters for performance
|
||||
# Most common/permissive policies first
|
||||
self.policies: List[PermissionPolicy] = [
|
||||
SuperuserPolicy(), # Check superuser first (most common bypass)
|
||||
OwnerPolicy(), # Then check ownership
|
||||
SelfAccessPolicy(), # Then self-access for user resources
|
||||
TenantPolicy(), # Finally tenant-level access
|
||||
]
|
||||
|
||||
def add_policy(self, policy: PermissionPolicy, position: Optional[int] = None):
|
||||
"""
|
||||
Add a permission policy to the chain.
|
||||
|
||||
Args:
|
||||
policy: The policy to add
|
||||
position: Optional position in the chain (default: append to end)
|
||||
"""
|
||||
if position is not None:
|
||||
self.policies.insert(position, policy)
|
||||
else:
|
||||
self.policies.append(policy)
|
||||
|
||||
def remove_policy(self, policy_class: type):
|
||||
"""
|
||||
Remove all policies of a specific class from the chain.
|
||||
|
||||
Args:
|
||||
policy_class: The class of policies to remove
|
||||
"""
|
||||
self.policies = [p for p in self.policies if not isinstance(p, policy_class)]
|
||||
|
||||
def can_perform(
|
||||
self,
|
||||
subject: Subject,
|
||||
action: Action,
|
||||
resource: Resource
|
||||
) -> bool:
|
||||
"""
|
||||
Check if a subject can perform an action on a resource.
|
||||
|
||||
Args:
|
||||
subject: The user/actor attempting the action
|
||||
action: The action being attempted
|
||||
resource: The resource being acted upon
|
||||
|
||||
Returns:
|
||||
True if any policy grants permission, False otherwise
|
||||
"""
|
||||
# Policy chain: any policy can grant permission (OR logic)
|
||||
for policy in self.policies:
|
||||
try:
|
||||
if policy.can_perform(subject, action, resource):
|
||||
logger.debug(
|
||||
f"permission_granted: policy={policy.__class__.__name__}, "
|
||||
f"subject_id={subject.id}, action={action.value}, "
|
||||
f"resource_type={resource.type.value}, resource_id={resource.id}"
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
# Log policy errors but continue checking other policies
|
||||
logger.error(
|
||||
f"permission_policy_error: policy={policy.__class__.__name__}, "
|
||||
f"error={str(e)}, subject_id={subject.id}, action={action.value}, "
|
||||
f"resource_type={resource.type.value}"
|
||||
)
|
||||
|
||||
logger.warning(
|
||||
f"permission_denied: subject_id={subject.id}, action={action.value}, "
|
||||
f"resource_type={resource.type.value}, resource_id={resource.id}, "
|
||||
f"subject_tenant={subject.tenant_id}, resource_tenant={resource.tenant_id}, "
|
||||
f"is_superuser={subject.is_superuser}"
|
||||
)
|
||||
return False
|
||||
|
||||
def require_permission(
|
||||
self,
|
||||
subject: Subject,
|
||||
action: Action,
|
||||
resource: Resource,
|
||||
error_message: Optional[str] = None
|
||||
):
|
||||
"""
|
||||
Require permission, raising an exception if not granted.
|
||||
|
||||
Args:
|
||||
subject: The user/actor attempting the action
|
||||
action: The action being attempted
|
||||
resource: The resource being acted upon
|
||||
error_message: Custom error message (optional)
|
||||
|
||||
Raises:
|
||||
PermissionDeniedException: If permission is not granted
|
||||
"""
|
||||
if not self.can_perform(subject, action, resource):
|
||||
message = error_message or (
|
||||
f"无权对 {resource.type.value} 执行 {action.value} 操作"
|
||||
)
|
||||
raise PermissionDeniedException(message)
|
||||
|
||||
def check_superuser(self, subject: Subject, error_message: Optional[str] = None):
|
||||
"""
|
||||
Require that the subject is a superuser.
|
||||
|
||||
Args:
|
||||
subject: The user/actor to check
|
||||
error_message: Custom error message (optional)
|
||||
|
||||
Raises:
|
||||
PermissionDeniedException: If subject is not a superuser
|
||||
"""
|
||||
if not subject.is_superuser:
|
||||
message = error_message or "需要超级管理员权限"
|
||||
logger.warning(
|
||||
f"superuser_required: subject_id={subject.id}, is_superuser={subject.is_superuser}"
|
||||
)
|
||||
raise PermissionDeniedException(message)
|
||||
|
||||
def check_same_tenant(
|
||||
self,
|
||||
subject: Subject,
|
||||
resource: Resource,
|
||||
error_message: Optional[str] = None
|
||||
):
|
||||
"""
|
||||
Require that the subject and resource are in the same tenant.
|
||||
|
||||
Args:
|
||||
subject: The user/actor to check
|
||||
resource: The resource to check
|
||||
error_message: Custom error message (optional)
|
||||
|
||||
Raises:
|
||||
PermissionDeniedException: If not in the same tenant
|
||||
"""
|
||||
if subject.tenant_id != resource.tenant_id:
|
||||
message = error_message or "无权访问其他租户的资源"
|
||||
logger.warning(
|
||||
f"tenant_mismatch: subject_id={subject.id}, "
|
||||
f"subject_tenant={subject.tenant_id}, resource_tenant={resource.tenant_id}"
|
||||
)
|
||||
raise PermissionDeniedException(message)
|
||||
|
||||
|
||||
# Global permission service instance
|
||||
permission_service = PermissionService()
|
||||
Reference in New Issue
Block a user