ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: backend enforcement workspace policy
This commit is contained in:
parent
9fa05d60b6
commit
e009915e34
|
|
@ -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:-}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"}),
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ from .base import BaseAPIView, BaseViewSet
|
|||
from .workspace.base import (
|
||||
WorkSpaceViewSet,
|
||||
UserWorkSpacesEndpoint,
|
||||
NodeDCWorkspaceCreationPolicyEndpoint,
|
||||
WorkSpaceAvailabilityCheckEndpoint,
|
||||
UserWorkspaceDashboardEndpoint,
|
||||
WorkspaceThemeViewSet,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
Loading…
Reference in New Issue