feat(permissions): centralize workspace role capability checks

This commit is contained in:
2026-04-25 18:48:50 +03:30
parent 5f9d413a57
commit f960ca8221
14 changed files with 925 additions and 222 deletions

View File

@@ -0,0 +1,71 @@
from apps.workspaces.services.permissions import (
CLIENTS_CREATE,
CLIENTS_DELETE,
CLIENTS_EDIT,
CLIENTS_VIEW,
PROJECTS_ARCHIVE,
PROJECTS_CREATE,
PROJECTS_DELETE,
PROJECTS_EDIT,
PROJECTS_VIEW,
PROJECT_MEMBERS_ADD,
PROJECT_MEMBERS_CHANGE_ROLE,
PROJECT_MEMBERS_REMOVE,
PROJECT_MEMBERS_VIEW,
TAGS_CREATE,
TAGS_DELETE,
TAGS_EDIT,
TAGS_VIEW,
TIME_ENTRIES_MANAGE_OWN,
TIME_ENTRIES_VIEW_OWN,
WORKSPACE_DELETE,
WORKSPACE_EDIT,
WORKSPACE_MEMBERS_ADD,
WORKSPACE_MEMBERS_CHANGE_ROLE,
WORKSPACE_MEMBERS_REMOVE,
WORKSPACE_MEMBERS_VIEW,
WORKSPACE_VIEW,
can_assign_workspace_role,
can_change_workspace_membership,
can_manage_workspace_members,
get_workspace_membership,
get_workspace_role,
has_project_capability,
has_workspace_capability,
)
__all__ = [
"WORKSPACE_VIEW",
"WORKSPACE_EDIT",
"WORKSPACE_DELETE",
"WORKSPACE_MEMBERS_VIEW",
"WORKSPACE_MEMBERS_ADD",
"WORKSPACE_MEMBERS_REMOVE",
"WORKSPACE_MEMBERS_CHANGE_ROLE",
"CLIENTS_VIEW",
"CLIENTS_CREATE",
"CLIENTS_EDIT",
"CLIENTS_DELETE",
"TAGS_VIEW",
"TAGS_CREATE",
"TAGS_EDIT",
"TAGS_DELETE",
"PROJECTS_VIEW",
"PROJECTS_CREATE",
"PROJECTS_EDIT",
"PROJECTS_DELETE",
"PROJECTS_ARCHIVE",
"PROJECT_MEMBERS_VIEW",
"PROJECT_MEMBERS_ADD",
"PROJECT_MEMBERS_REMOVE",
"PROJECT_MEMBERS_CHANGE_ROLE",
"TIME_ENTRIES_VIEW_OWN",
"TIME_ENTRIES_MANAGE_OWN",
"get_workspace_membership",
"get_workspace_role",
"has_workspace_capability",
"has_project_capability",
"can_manage_workspace_members",
"can_assign_workspace_role",
"can_change_workspace_membership",
]

View File

@@ -0,0 +1,210 @@
from __future__ import annotations
from apps.projects.models import ProjectMembership
from apps.workspaces.models import Workspace, WorkspaceMembership
WORKSPACE_VIEW = "workspace.view"
WORKSPACE_EDIT = "workspace.edit"
WORKSPACE_DELETE = "workspace.delete"
WORKSPACE_MEMBERS_VIEW = "workspace.members.view"
WORKSPACE_MEMBERS_ADD = "workspace.members.add"
WORKSPACE_MEMBERS_REMOVE = "workspace.members.remove"
WORKSPACE_MEMBERS_CHANGE_ROLE = "workspace.members.change_role"
CLIENTS_VIEW = "clients.view"
CLIENTS_CREATE = "clients.create"
CLIENTS_EDIT = "clients.edit"
CLIENTS_DELETE = "clients.delete"
TAGS_VIEW = "tags.view"
TAGS_CREATE = "tags.create"
TAGS_EDIT = "tags.edit"
TAGS_DELETE = "tags.delete"
PROJECTS_VIEW = "projects.view"
PROJECTS_CREATE = "projects.create"
PROJECTS_EDIT = "projects.edit"
PROJECTS_DELETE = "projects.delete"
PROJECTS_ARCHIVE = "projects.archive"
PROJECT_MEMBERS_VIEW = "project_members.view"
PROJECT_MEMBERS_ADD = "project_members.add"
PROJECT_MEMBERS_REMOVE = "project_members.remove"
PROJECT_MEMBERS_CHANGE_ROLE = "project_members.change_role"
TIME_ENTRIES_VIEW_OWN = "time_entries.view_own"
TIME_ENTRIES_MANAGE_OWN = "time_entries.manage_own"
PROJECT_MANAGER_CAPABILITIES = {
PROJECTS_EDIT,
PROJECTS_ARCHIVE,
PROJECT_MEMBERS_VIEW,
PROJECT_MEMBERS_ADD,
PROJECT_MEMBERS_REMOVE,
PROJECT_MEMBERS_CHANGE_ROLE,
}
WORKSPACE_ROLE_CAPABILITIES = {
WorkspaceMembership.Role.OWNER: {
WORKSPACE_VIEW,
WORKSPACE_EDIT,
WORKSPACE_DELETE,
WORKSPACE_MEMBERS_VIEW,
WORKSPACE_MEMBERS_ADD,
WORKSPACE_MEMBERS_REMOVE,
WORKSPACE_MEMBERS_CHANGE_ROLE,
CLIENTS_VIEW,
CLIENTS_CREATE,
CLIENTS_EDIT,
CLIENTS_DELETE,
TAGS_VIEW,
TAGS_CREATE,
TAGS_EDIT,
TAGS_DELETE,
PROJECTS_VIEW,
PROJECTS_CREATE,
PROJECTS_EDIT,
PROJECTS_DELETE,
PROJECTS_ARCHIVE,
PROJECT_MEMBERS_VIEW,
PROJECT_MEMBERS_ADD,
PROJECT_MEMBERS_REMOVE,
PROJECT_MEMBERS_CHANGE_ROLE,
TIME_ENTRIES_VIEW_OWN,
TIME_ENTRIES_MANAGE_OWN,
},
WorkspaceMembership.Role.ADMIN: {
WORKSPACE_VIEW,
WORKSPACE_EDIT,
WORKSPACE_MEMBERS_VIEW,
WORKSPACE_MEMBERS_ADD,
WORKSPACE_MEMBERS_REMOVE,
WORKSPACE_MEMBERS_CHANGE_ROLE,
CLIENTS_VIEW,
CLIENTS_CREATE,
CLIENTS_EDIT,
CLIENTS_DELETE,
TAGS_VIEW,
TAGS_CREATE,
TAGS_EDIT,
TAGS_DELETE,
PROJECTS_VIEW,
PROJECTS_CREATE,
PROJECTS_EDIT,
PROJECTS_DELETE,
PROJECTS_ARCHIVE,
PROJECT_MEMBERS_VIEW,
PROJECT_MEMBERS_ADD,
PROJECT_MEMBERS_REMOVE,
PROJECT_MEMBERS_CHANGE_ROLE,
TIME_ENTRIES_VIEW_OWN,
TIME_ENTRIES_MANAGE_OWN,
},
WorkspaceMembership.Role.MEMBER: {
WORKSPACE_VIEW,
CLIENTS_VIEW,
TAGS_VIEW,
TAGS_CREATE,
PROJECTS_VIEW,
TIME_ENTRIES_VIEW_OWN,
TIME_ENTRIES_MANAGE_OWN,
},
WorkspaceMembership.Role.GUEST: {
WORKSPACE_VIEW,
CLIENTS_VIEW,
TAGS_VIEW,
PROJECTS_VIEW,
TIME_ENTRIES_VIEW_OWN,
},
}
def get_workspace_membership(user, workspace: Workspace) -> WorkspaceMembership | None:
if not user or not user.is_authenticated:
return None
return WorkspaceMembership.objects.filter(
workspace=workspace,
user=user,
is_active=True,
is_deleted=False,
).first()
def get_workspace_role(user, workspace: Workspace) -> str | None:
if not user or not user.is_authenticated:
return None
if workspace.owner_id == user.id:
return WorkspaceMembership.Role.OWNER
membership = get_workspace_membership(user, workspace)
return getattr(membership, "role", None)
def has_workspace_capability(user, workspace: Workspace, capability: str) -> bool:
role = get_workspace_role(user, workspace)
if not role:
return False
return capability in WORKSPACE_ROLE_CAPABILITIES.get(role, set())
def has_project_capability(user, project, capability: str) -> bool:
if has_workspace_capability(user, project.workspace, capability):
return True
workspace_role = get_workspace_role(user, project.workspace)
if workspace_role not in {
WorkspaceMembership.Role.OWNER,
WorkspaceMembership.Role.ADMIN,
}:
return False
is_project_manager = ProjectMembership.objects.filter(
project=project,
user=user,
role=ProjectMembership.Role.MANAGER,
is_active=True,
is_deleted=False,
).exists()
return is_project_manager and capability in PROJECT_MANAGER_CAPABILITIES
def can_manage_workspace_members(user, workspace: Workspace) -> bool:
return has_workspace_capability(user, workspace, WORKSPACE_MEMBERS_CHANGE_ROLE)
def can_assign_workspace_role(user, workspace: Workspace, role: str) -> bool:
actor_role = get_workspace_role(user, workspace)
if actor_role == WorkspaceMembership.Role.OWNER:
return True
if actor_role == WorkspaceMembership.Role.ADMIN:
return role != WorkspaceMembership.Role.OWNER
return False
def can_change_workspace_membership(user, membership: WorkspaceMembership, *, new_role: str | None = None) -> bool:
workspace = membership.workspace
actor_role = get_workspace_role(user, workspace)
if actor_role not in {
WorkspaceMembership.Role.OWNER,
WorkspaceMembership.Role.ADMIN,
}:
return False
if membership.user_id == user.id:
return False
target_is_canonical_owner = workspace.owner_id == membership.user_id
target_is_owner_role = membership.role == WorkspaceMembership.Role.OWNER
if actor_role == WorkspaceMembership.Role.ADMIN:
if target_is_owner_role or target_is_canonical_owner:
return False
if new_role == WorkspaceMembership.Role.OWNER:
return False
return True
if target_is_canonical_owner:
return False
if new_role == WorkspaceMembership.Role.OWNER and workspace.owner_id != user.id:
return False
return True