АРХ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: managedBy split Operational Core
This commit is contained in:
@@ -9,6 +9,10 @@ from django.db.models import Min
|
||||
|
||||
# Module imports
|
||||
from .base import BaseViewSet, BaseAPIView
|
||||
from plane.authentication.nodedc_workspace_policy import (
|
||||
is_nodedc_launcher_managed_workspace,
|
||||
nodedc_launcher_managed_workspace_response,
|
||||
)
|
||||
from plane.app.serializers import (
|
||||
ProjectMemberSerializer,
|
||||
ProjectMemberAdminSerializer,
|
||||
@@ -45,6 +49,9 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
|
||||
@allow_permission([ROLE.ADMIN])
|
||||
def create(self, request, slug, project_id):
|
||||
if is_nodedc_launcher_managed_workspace(request.user, slug):
|
||||
return Response(nodedc_launcher_managed_workspace_response(), status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# Get the list of members to be added to the project and their roles i.e. the user_id and the role
|
||||
members = request.data.get("members", [])
|
||||
|
||||
@@ -204,6 +211,9 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
if is_nodedc_launcher_managed_workspace(request.user, slug):
|
||||
return Response(nodedc_launcher_managed_workspace_response(), status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
project_member = ProjectMember.objects.get(pk=pk, workspace__slug=slug, project_id=project_id, is_active=True)
|
||||
|
||||
# Fetch the workspace role of the project member
|
||||
@@ -266,6 +276,9 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
|
||||
@allow_permission([ROLE.ADMIN])
|
||||
def destroy(self, request, slug, project_id, pk):
|
||||
if is_nodedc_launcher_managed_workspace(request.user, slug):
|
||||
return Response(nodedc_launcher_managed_workspace_response(), status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
project_member = ProjectMember.objects.get(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
@@ -300,6 +313,9 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def leave(self, request, slug, project_id):
|
||||
if is_nodedc_launcher_managed_workspace(request.user, slug):
|
||||
return Response(nodedc_launcher_managed_workspace_response(), status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
project_member = ProjectMember.objects.get(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
|
||||
@@ -257,7 +257,8 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
||||
|
||||
class NodeDCWorkspaceCreationPolicyEndpoint(BaseAPIView):
|
||||
def get(self, request):
|
||||
return Response(get_nodedc_workspace_creation_policy(request.user), status=status.HTTP_200_OK)
|
||||
workspace_slug = request.query_params.get("workspace_slug") or request.query_params.get("workspaceSlug")
|
||||
return Response(get_nodedc_workspace_creation_policy(request.user, workspace_slug=workspace_slug), status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView):
|
||||
|
||||
@@ -20,6 +20,10 @@ from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from plane.app.permissions import WorkSpaceAdminPermission
|
||||
from plane.authentication.nodedc_workspace_policy import (
|
||||
is_nodedc_launcher_managed_workspace,
|
||||
nodedc_launcher_managed_workspace_response,
|
||||
)
|
||||
from plane.app.serializers import (
|
||||
WorkSpaceMemberInviteSerializer,
|
||||
WorkSpaceMemberSerializer,
|
||||
@@ -52,6 +56,9 @@ class WorkspaceInvitationsViewset(BaseViewSet):
|
||||
)
|
||||
|
||||
def create(self, request, slug):
|
||||
if is_nodedc_launcher_managed_workspace(request.user, slug):
|
||||
return Response(nodedc_launcher_managed_workspace_response(), status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
emails = request.data.get("emails", [])
|
||||
# Check if email is provided
|
||||
if not emails:
|
||||
@@ -154,6 +161,9 @@ class WorkspaceInvitationsViewset(BaseViewSet):
|
||||
return Response({"message": "Emails sent successfully"}, status=status.HTTP_200_OK)
|
||||
|
||||
def destroy(self, request, slug, pk):
|
||||
if is_nodedc_launcher_managed_workspace(request.user, slug):
|
||||
return Response(nodedc_launcher_managed_workspace_response(), status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
workspace_member_invite = WorkspaceMemberInvite.objects.get(pk=pk, workspace__slug=slug)
|
||||
workspace_member_invite.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -12,6 +12,10 @@ from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
from plane.app.permissions import WorkspaceEntityPermission, allow_permission, ROLE
|
||||
from plane.authentication.nodedc_workspace_policy import (
|
||||
is_nodedc_launcher_managed_workspace,
|
||||
nodedc_launcher_managed_workspace_response,
|
||||
)
|
||||
|
||||
# Module imports
|
||||
from plane.app.serializers import (
|
||||
@@ -75,6 +79,9 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
|
||||
def partial_update(self, request, slug, pk):
|
||||
if is_nodedc_launcher_managed_workspace(request.user, slug):
|
||||
return Response(nodedc_launcher_managed_workspace_response(), status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
workspace_member = WorkspaceMember.objects.get(
|
||||
pk=pk, workspace__slug=slug, member__is_bot=False, is_active=True
|
||||
)
|
||||
@@ -97,6 +104,9 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
|
||||
def destroy(self, request, slug, pk):
|
||||
if is_nodedc_launcher_managed_workspace(request.user, slug):
|
||||
return Response(nodedc_launcher_managed_workspace_response(), status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# Check the user role who is deleting the user
|
||||
workspace_member = WorkspaceMember.objects.get(
|
||||
workspace__slug=slug, pk=pk, member__is_bot=False, is_active=True
|
||||
@@ -160,6 +170,9 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
@invalidate_cache(path="api/users/me/workspaces/", user=False, multiple=True)
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def leave(self, request, slug):
|
||||
if is_nodedc_launcher_managed_workspace(request.user, slug):
|
||||
return Response(nodedc_launcher_managed_workspace_response(), status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
workspace_member = WorkspaceMember.objects.get(workspace__slug=slug, member=request.user, is_active=True)
|
||||
|
||||
# Check if the leaving user is the only admin of the workspace
|
||||
|
||||
@@ -9,7 +9,7 @@ from plane.db.models import ExternalIdentityLink
|
||||
OIDC_PROVIDER = "authentik"
|
||||
|
||||
|
||||
def get_nodedc_workspace_creation_policy(user):
|
||||
def get_nodedc_workspace_creation_policy(user, workspace_slug=None):
|
||||
check_url = (
|
||||
os.environ.get("PLANE_NODEDC_WORKSPACE_POLICY_URL", "").strip()
|
||||
or os.environ.get("PLANE_NODEDC_ACCESS_CHECK_URL", "").strip()
|
||||
@@ -21,6 +21,9 @@ def get_nodedc_workspace_creation_policy(user):
|
||||
"enabled": False,
|
||||
"can_create_workspace": True,
|
||||
"mode": "standalone",
|
||||
"managed_by": "tasker",
|
||||
"default_managed_by": "tasker",
|
||||
"workspaces": [],
|
||||
"reason": "NODE.DC workspace policy is not configured.",
|
||||
}
|
||||
|
||||
@@ -36,6 +39,9 @@ def get_nodedc_workspace_creation_policy(user):
|
||||
"enabled": True,
|
||||
"can_create_workspace": not enforce_unlinked,
|
||||
"mode": "unlinked",
|
||||
"managed_by": "tasker",
|
||||
"default_managed_by": "tasker",
|
||||
"workspaces": [],
|
||||
"reason": "NODE.DC identity is not linked." if enforce_unlinked else "Standalone user without NODE.DC identity.",
|
||||
}
|
||||
|
||||
@@ -61,6 +67,9 @@ def get_nodedc_workspace_creation_policy(user):
|
||||
"enabled": True,
|
||||
"can_create_workspace": False,
|
||||
"mode": "unavailable",
|
||||
"managed_by": "tasker",
|
||||
"default_managed_by": "tasker",
|
||||
"workspaces": [],
|
||||
"reason": "NODE.DC workspace policy is unavailable.",
|
||||
}
|
||||
|
||||
@@ -71,18 +80,84 @@ def get_nodedc_workspace_creation_policy(user):
|
||||
"enabled": True,
|
||||
"can_create_workspace": access_allowed,
|
||||
"mode": "legacy_access_check",
|
||||
"managed_by": "tasker",
|
||||
"default_managed_by": "tasker",
|
||||
"workspaces": [],
|
||||
"reason": payload.get("reason") or "NODE.DC access check does not expose workspace policy.",
|
||||
}
|
||||
|
||||
can_create_workspace = access_allowed and bool(workspace_policy.get("canCreateWorkspace"))
|
||||
workspaces = normalize_workspace_management_list(workspace_policy.get("workspaces"))
|
||||
managed_by = resolve_workspace_managed_by(
|
||||
workspace_slug=workspace_slug,
|
||||
workspaces=workspaces,
|
||||
fallback=workspace_policy.get("managedBy") or workspace_policy.get("defaultManagedBy"),
|
||||
)
|
||||
|
||||
return {
|
||||
"enabled": True,
|
||||
"can_create_workspace": can_create_workspace,
|
||||
"mode": workspace_policy.get("mode") or "unknown",
|
||||
"managed_by": managed_by,
|
||||
"default_managed_by": normalize_managed_by(workspace_policy.get("defaultManagedBy") or workspace_policy.get("managedBy")),
|
||||
"workspaces": workspaces,
|
||||
"reason": workspace_policy.get("reason") or payload.get("reason") or "NODE.DC workspace policy decision.",
|
||||
}
|
||||
|
||||
|
||||
def is_truthy(value):
|
||||
return str(value).strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
def normalize_managed_by(value):
|
||||
return "launcher" if value == "launcher" else "tasker"
|
||||
|
||||
|
||||
def normalize_workspace_management_list(value):
|
||||
if not isinstance(value, list):
|
||||
return []
|
||||
|
||||
workspaces = []
|
||||
for item in value:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
slug = item.get("slug")
|
||||
if not isinstance(slug, str) or not slug.strip():
|
||||
continue
|
||||
workspaces.append(
|
||||
{
|
||||
"slug": slug.strip(),
|
||||
"name": item.get("name") if isinstance(item.get("name"), str) and item.get("name").strip() else None,
|
||||
"managed_by": normalize_managed_by(item.get("managedBy") or item.get("managed_by")),
|
||||
"client_id": item.get("clientId") if isinstance(item.get("clientId"), str) else None,
|
||||
"client_name": item.get("clientName") if isinstance(item.get("clientName"), str) else None,
|
||||
"role": item.get("role") if item.get("role") in {"guest", "member", "admin"} else "member",
|
||||
}
|
||||
)
|
||||
|
||||
return workspaces
|
||||
|
||||
|
||||
def resolve_workspace_managed_by(workspace_slug, workspaces, fallback):
|
||||
if isinstance(workspace_slug, str) and workspace_slug.strip():
|
||||
normalized_slug = workspace_slug.strip()
|
||||
for workspace in workspaces:
|
||||
if workspace["slug"] == normalized_slug:
|
||||
return workspace["managed_by"]
|
||||
return "tasker"
|
||||
|
||||
return normalize_managed_by(fallback)
|
||||
|
||||
|
||||
def is_nodedc_launcher_managed_workspace(user, workspace_slug):
|
||||
policy = get_nodedc_workspace_creation_policy(user, workspace_slug=workspace_slug)
|
||||
return bool(policy.get("enabled")) and (
|
||||
policy.get("managed_by") == "launcher" or policy.get("mode") == "unavailable"
|
||||
)
|
||||
|
||||
|
||||
def nodedc_launcher_managed_workspace_response():
|
||||
return {
|
||||
"error": "nodedc_launcher_managed_workspace",
|
||||
"reason": "Участниками и ролями этого workspace управляет Launcher.",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user