ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: 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_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_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_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
|
GUNICORN_WORKERS: 1
|
||||||
POSTHOG_API_KEY: ${POSTHOG_API_KEY:-}
|
POSTHOG_API_KEY: ${POSTHOG_API_KEY:-}
|
||||||
POSTHOG_HOST: ${POSTHOG_HOST:-}
|
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_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_URL=http://launcher.local.nodedc/api/internal/handoff/consume
|
||||||
PLANE_NODEDC_HANDOFF_TIMEOUT_SECONDS=3
|
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 (
|
from plane.app.views import (
|
||||||
UserWorkspaceInvitationsViewSet,
|
UserWorkspaceInvitationsViewSet,
|
||||||
|
NodeDCWorkspaceCreationPolicyEndpoint,
|
||||||
WorkSpaceViewSet,
|
WorkSpaceViewSet,
|
||||||
WorkspaceJoinEndpoint,
|
WorkspaceJoinEndpoint,
|
||||||
WorkSpaceMemberViewSet,
|
WorkSpaceMemberViewSet,
|
||||||
|
|
@ -49,6 +50,11 @@ urlpatterns = [
|
||||||
WorkSpaceAvailabilityCheckEndpoint.as_view(),
|
WorkSpaceAvailabilityCheckEndpoint.as_view(),
|
||||||
name="workspace-availability",
|
name="workspace-availability",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"nodedc/workspace-policy/",
|
||||||
|
NodeDCWorkspaceCreationPolicyEndpoint.as_view(),
|
||||||
|
name="nodedc-workspace-policy",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/",
|
"workspaces/",
|
||||||
WorkSpaceViewSet.as_view({"get": "list", "post": "create"}),
|
WorkSpaceViewSet.as_view({"get": "list", "post": "create"}),
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ from .base import BaseAPIView, BaseViewSet
|
||||||
from .workspace.base import (
|
from .workspace.base import (
|
||||||
WorkSpaceViewSet,
|
WorkSpaceViewSet,
|
||||||
UserWorkSpacesEndpoint,
|
UserWorkSpacesEndpoint,
|
||||||
|
NodeDCWorkspaceCreationPolicyEndpoint,
|
||||||
WorkSpaceAvailabilityCheckEndpoint,
|
WorkSpaceAvailabilityCheckEndpoint,
|
||||||
UserWorkspaceDashboardEndpoint,
|
UserWorkspaceDashboardEndpoint,
|
||||||
WorkspaceThemeViewSet,
|
WorkspaceThemeViewSet,
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ from plane.app.permissions import (
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.app.serializers import WorkSpaceSerializer, WorkspaceThemeSerializer
|
from plane.app.serializers import WorkSpaceSerializer, WorkspaceThemeSerializer
|
||||||
from plane.app.views.base import BaseAPIView, BaseViewSet
|
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 (
|
from plane.db.models import (
|
||||||
Issue,
|
Issue,
|
||||||
IssueActivity,
|
IssueActivity,
|
||||||
|
|
@ -83,6 +84,17 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||||
|
|
||||||
def create(self, request):
|
def create(self, request):
|
||||||
try:
|
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(
|
(DISABLE_WORKSPACE_CREATION,) = get_configuration_value(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|
@ -243,6 +255,11 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
||||||
return Response(workspaces, status=status.HTTP_200_OK)
|
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):
|
class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
slug = request.GET.get("slug", False)
|
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