Initial commit

This commit is contained in:
Ke Sun
2025-11-30 18:22:17 +08:00
commit aea2fe391e
449 changed files with 83030 additions and 0 deletions

View 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",
]

View 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()
)

View 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
)

View 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()