ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: backend enforcement workspace policy

This commit is contained in:
DCCONSTRUCTIONS 2026-05-06 09:38:14 +03:00
parent 9fa05d60b6
commit e009915e34
6 changed files with 116 additions and 0 deletions

View File

@ -74,6 +74,8 @@ x-app-env: &app-env
PLANE_NODEDC_GLOBAL_LOGOUT_URL: ${PLANE_NODEDC_GLOBAL_LOGOUT_URL:-http://launcher.local.nodedc/auth/logout?global=1&returnTo=/}
PLANE_NODEDC_HANDOFF_URL: ${PLANE_NODEDC_HANDOFF_URL:-http://launcher.local.nodedc/api/internal/handoff/consume}
PLANE_NODEDC_HANDOFF_TIMEOUT_SECONDS: ${PLANE_NODEDC_HANDOFF_TIMEOUT_SECONDS:-3}
PLANE_NODEDC_WORKSPACE_POLICY_URL: ${PLANE_NODEDC_WORKSPACE_POLICY_URL:-http://launcher.local.nodedc/api/internal/access/check}
PLANE_NODEDC_WORKSPACE_POLICY_TIMEOUT_SECONDS: ${PLANE_NODEDC_WORKSPACE_POLICY_TIMEOUT_SECONDS:-3}
GUNICORN_WORKERS: 1
POSTHOG_API_KEY: ${POSTHOG_API_KEY:-}
POSTHOG_HOST: ${POSTHOG_HOST:-}

View File

@ -113,3 +113,5 @@ PLANE_NODEDC_ACCESS_DENIED_REDIRECT_URL=http://launcher.local.nodedc/
PLANE_NODEDC_GLOBAL_LOGOUT_URL=http://launcher.local.nodedc/auth/logout?global=1&returnTo=/
PLANE_NODEDC_HANDOFF_URL=http://launcher.local.nodedc/api/internal/handoff/consume
PLANE_NODEDC_HANDOFF_TIMEOUT_SECONDS=3
PLANE_NODEDC_WORKSPACE_POLICY_URL=http://launcher.local.nodedc/api/internal/access/check
PLANE_NODEDC_WORKSPACE_POLICY_TIMEOUT_SECONDS=3

View File

@ -7,6 +7,7 @@ from django.urls import path
from plane.app.views import (
UserWorkspaceInvitationsViewSet,
NodeDCWorkspaceCreationPolicyEndpoint,
WorkSpaceViewSet,
WorkspaceJoinEndpoint,
WorkSpaceMemberViewSet,
@ -49,6 +50,11 @@ urlpatterns = [
WorkSpaceAvailabilityCheckEndpoint.as_view(),
name="workspace-availability",
),
path(
"nodedc/workspace-policy/",
NodeDCWorkspaceCreationPolicyEndpoint.as_view(),
name="nodedc-workspace-policy",
),
path(
"workspaces/",
WorkSpaceViewSet.as_view({"get": "list", "post": "create"}),

View File

@ -37,6 +37,7 @@ from .base import BaseAPIView, BaseViewSet
from .workspace.base import (
WorkSpaceViewSet,
UserWorkSpacesEndpoint,
NodeDCWorkspaceCreationPolicyEndpoint,
WorkSpaceAvailabilityCheckEndpoint,
UserWorkspaceDashboardEndpoint,
WorkspaceThemeViewSet,

View File

@ -34,6 +34,7 @@ from plane.app.permissions import (
# Module imports
from plane.app.serializers import WorkSpaceSerializer, WorkspaceThemeSerializer
from plane.app.views.base import BaseAPIView, BaseViewSet
from plane.authentication.nodedc_workspace_policy import get_nodedc_workspace_creation_policy
from plane.db.models import (
Issue,
IssueActivity,
@ -83,6 +84,17 @@ class WorkSpaceViewSet(BaseViewSet):
def create(self, request):
try:
workspace_policy = get_nodedc_workspace_creation_policy(request.user)
if not workspace_policy["can_create_workspace"]:
return Response(
{
"error": "nodedc_workspace_creation_denied",
"reason": workspace_policy["reason"],
"workspace_policy": workspace_policy,
},
status=status.HTTP_403_FORBIDDEN,
)
(DISABLE_WORKSPACE_CREATION,) = get_configuration_value(
[
{
@ -243,6 +255,11 @@ class UserWorkSpacesEndpoint(BaseAPIView):
return Response(workspaces, status=status.HTTP_200_OK)
class NodeDCWorkspaceCreationPolicyEndpoint(BaseAPIView):
def get(self, request):
return Response(get_nodedc_workspace_creation_policy(request.user), status=status.HTTP_200_OK)
class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView):
def get(self, request):
slug = request.GET.get("slug", False)

View File

@ -0,0 +1,88 @@
import os
import requests
from plane.authentication.views.nodedc_logout import get_nodedc_internal_token
from plane.db.models import ExternalIdentityLink
OIDC_PROVIDER = "authentik"
def get_nodedc_workspace_creation_policy(user):
check_url = (
os.environ.get("PLANE_NODEDC_WORKSPACE_POLICY_URL", "").strip()
or os.environ.get("PLANE_NODEDC_ACCESS_CHECK_URL", "").strip()
)
token = get_nodedc_internal_token()
if not check_url or not token:
return {
"enabled": False,
"can_create_workspace": True,
"mode": "standalone",
"reason": "NODE.DC workspace policy is not configured.",
}
link = ExternalIdentityLink.objects.filter(
provider=OIDC_PROVIDER,
user=user,
status=ExternalIdentityLink.Status.ACTIVE,
).first()
if link is None:
enforce_unlinked = is_truthy(os.environ.get("PLANE_NODEDC_WORKSPACE_POLICY_ENFORCE_UNLINKED", "0"))
return {
"enabled": True,
"can_create_workspace": not enforce_unlinked,
"mode": "unlinked",
"reason": "NODE.DC identity is not linked." if enforce_unlinked else "Standalone user without NODE.DC identity.",
}
try:
response = requests.post(
check_url,
json={
"serviceSlug": os.environ.get("PLANE_NODEDC_ACCESS_SERVICE_SLUG", "task-manager"),
"subject": link.subject,
"email": link.email or user.email,
"userId": None,
},
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/json",
},
timeout=float(os.environ.get("PLANE_NODEDC_WORKSPACE_POLICY_TIMEOUT_SECONDS", "3") or "3"),
)
response.raise_for_status()
payload = response.json()
except (ValueError, requests.RequestException):
return {
"enabled": True,
"can_create_workspace": False,
"mode": "unavailable",
"reason": "NODE.DC workspace policy is unavailable.",
}
workspace_policy = payload.get("workspacePolicy") if isinstance(payload.get("workspacePolicy"), dict) else {}
access_allowed = bool(payload.get("allowed"))
if not workspace_policy:
return {
"enabled": True,
"can_create_workspace": access_allowed,
"mode": "legacy_access_check",
"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"))
return {
"enabled": True,
"can_create_workspace": can_create_workspace,
"mode": workspace_policy.get("mode") or "unknown",
"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"}