SEC - TASKER: enforce workspace-scoped agent access

This commit is contained in:
DCCONSTRUCTIONS
2026-05-15 00:47:59 +03:00
parent 65ee15b86c
commit b0a682b63b
5 changed files with 691 additions and 147 deletions
@@ -1,5 +1,6 @@
from html import escape
from django.core.exceptions import ValidationError
from django.db import transaction
from django.http import JsonResponse
from django.utils import timezone
@@ -9,6 +10,7 @@ from django.views.decorators.csrf import csrf_exempt
from plane.app.realtime.issue_events import publish_issue_event_on_commit
from plane.authentication.views.nodedc_logout import is_internal_logout_request_authorized
from plane.authentication.nodedc_workspace_policy import get_nodedc_workspace_creation_policy
from plane.authentication.views.nodedc_workspace_adapter import parse_json_body
from plane.db.models import (
Issue,
@@ -244,6 +246,33 @@ def validate_internal_request(request):
return None
def resolve_agent_owner(request):
identity = get_agent_identity(request)
if identity is None:
return None
try:
return User.objects.filter(id=identity["owner_user_id"], is_active=True).first()
except (TypeError, ValueError, ValidationError):
return None
def validate_agent_workspace_entitlement(request, workspace_slug):
if not workspace_slug:
return validation_error("workspace_slug_required")
owner = resolve_agent_owner(request)
if owner is None:
return validation_error("agent_owner_not_found", status=403)
workspace_policy = get_nodedc_workspace_creation_policy(owner, workspace_slug=workspace_slug)
service_modules = workspace_policy.get("service_modules") or {}
if service_modules.get("codex_agents") is not True:
return validation_error("codex_agents_not_entitled", status=403)
return None
def html_from_text(value):
text = value.strip() if isinstance(value, str) else ""
return f"<p>{escape(text)}</p>" if text else "<p></p>"
@@ -319,6 +348,9 @@ class NodeDCAgentProjectResolveEndpoint(View):
project_id = grant.get("project_id")
if not isinstance(workspace_slug, str) or not workspace_slug:
continue
entitlement_error = validate_agent_workspace_entitlement(request, workspace_slug)
if entitlement_error is not None:
continue
if isinstance(project_id, str) and project_id:
project_filters.append((workspace_slug, project_id))
else:
@@ -358,6 +390,10 @@ class NodeDCAgentProjectContextEndpoint(View):
if project is None:
return validation_error("project_not_found", status=404)
entitlement_error = validate_agent_workspace_entitlement(request, project.workspace.slug)
if entitlement_error is not None:
return entitlement_error
states = State.objects.filter(project=project, deleted_at__isnull=True).order_by("sequence")
labels = Label.objects.filter(project=project, deleted_at__isnull=True).order_by("sort_order")
members = (
@@ -392,6 +428,10 @@ class NodeDCAgentIssueListEndpoint(View):
if project is None:
return validation_error("project_not_found", status=404)
entitlement_error = validate_agent_workspace_entitlement(request, project.workspace.slug)
if entitlement_error is not None:
return entitlement_error
queryset = (
Issue.issue_objects.filter(project=project)
.select_related("workspace", "project", "state")
@@ -416,6 +456,10 @@ class NodeDCAgentIssueListEndpoint(View):
if project is None:
return validation_error("project_not_found", status=404)
entitlement_error = validate_agent_workspace_entitlement(request, project.workspace.slug)
if entitlement_error is not None:
return entitlement_error
title = payload.get("title")
if not isinstance(title, str) or not title.strip():
return validation_error("title_required")
@@ -466,6 +510,10 @@ class NodeDCAgentIssueUpdateEndpoint(View):
if issue is None:
return validation_error("issue_not_found", status=404)
entitlement_error = validate_agent_workspace_entitlement(request, project.workspace.slug)
if entitlement_error is not None:
return entitlement_error
detail_layout = (
merge_structured_blocks(issue.detail_layout, payload.get("structured_blocks"))
if "structured_blocks" in payload
@@ -519,6 +567,10 @@ class NodeDCAgentIssueMoveEndpoint(View):
if issue is None:
return validation_error("issue_not_found", status=404)
entitlement_error = validate_agent_workspace_entitlement(request, project.workspace.slug)
if entitlement_error is not None:
return entitlement_error
state = State.objects.filter(project=project, id=payload.get("state_id"), deleted_at__isnull=True).first()
if state is None:
return validation_error("state_not_found", status=404)
@@ -555,6 +607,10 @@ class NodeDCAgentIssueCommentEndpoint(View):
if issue is None:
return validation_error("issue_not_found", status=404)
entitlement_error = validate_agent_workspace_entitlement(request, project.workspace.slug)
if entitlement_error is not None:
return entitlement_error
body = payload.get("body")
if not isinstance(body, str) or not body.strip():
return validation_error("body_required")
@@ -592,6 +648,10 @@ class NodeDCAgentIssueLabelsEndpoint(View):
if issue is None:
return validation_error("issue_not_found", status=404)
entitlement_error = validate_agent_workspace_entitlement(request, project.workspace.slug)
if entitlement_error is not None:
return entitlement_error
label_ids = payload.get("label_ids")
if not isinstance(label_ids, list):
return validation_error("label_ids_required")
@@ -625,6 +685,10 @@ class NodeDCAgentIssueAssigneesEndpoint(View):
if issue is None:
return validation_error("issue_not_found", status=404)
entitlement_error = validate_agent_workspace_entitlement(request, project.workspace.slug)
if entitlement_error is not None:
return entitlement_error
member_ids = payload.get("member_ids")
if not isinstance(member_ids, list):
return validation_error("member_ids_required")