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
@@ -12,7 +12,7 @@ from rest_framework.response import Response
from plane.app.permissions import ROLE, allow_permission
from plane.app.views.base import BaseAPIView
from plane.authentication.nodedc_workspace_policy import get_nodedc_workspace_creation_policy
from plane.db.models import Project, Workspace
from plane.db.models import Project, ProjectMember, Workspace, WorkspaceMember
def get_gateway_config():
@@ -80,16 +80,35 @@ def require_workspace(slug):
)
def validate_project_in_workspace(workspace, project_id):
def is_workspace_admin(user, workspace):
return WorkspaceMember.objects.filter(
workspace=workspace,
member=user,
role=ROLE.ADMIN.value,
is_active=True,
).exists()
def validate_project_in_workspace(workspace, project_id, user):
workspace_admin = is_workspace_admin(user, workspace)
if not project_id:
if not workspace_admin:
return Response(
{
"ok": False,
"error": "project_required",
"message": "Workspace members must select a concrete project for Codex Agent grants.",
},
status=status.HTTP_400_BAD_REQUEST,
)
return None
try:
exists = Project.objects.filter(id=project_id, workspace=workspace, archived_at__isnull=True).exists()
project = Project.objects.filter(id=project_id, workspace=workspace, archived_at__isnull=True).first()
except ValidationError:
exists = False
project = None
if not exists:
if project is None:
return Response(
{
"ok": False,
@@ -98,6 +117,25 @@ def validate_project_in_workspace(workspace, project_id):
},
status=status.HTTP_404_NOT_FOUND,
)
if workspace_admin:
return None
if not ProjectMember.objects.filter(
project=project,
member=user,
role__gte=ROLE.MEMBER.value,
is_active=True,
).exists():
return Response(
{
"ok": False,
"error": "project_access_denied",
"message": "Codex Agent grants are limited to projects where this user is an active project member.",
},
status=status.HTTP_403_FORBIDDEN,
)
return None
@@ -147,7 +185,7 @@ class CodexAgentEntitledEndpoint(BaseAPIView):
class CodexAgentListEndpoint(CodexAgentEntitledEndpoint):
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def get(self, request, slug):
entitlement_error = self.require_entitlement(request, slug)
if entitlement_error is not None:
@@ -155,7 +193,7 @@ class CodexAgentListEndpoint(CodexAgentEntitledEndpoint):
return gateway_request("GET", f"/api/internal/v1/owners/{owner_path(request.user)}/agents")
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def post(self, request, slug):
entitlement_error = self.require_entitlement(request, slug)
if entitlement_error is not None:
@@ -177,7 +215,7 @@ class CodexAgentListEndpoint(CodexAgentEntitledEndpoint):
class CodexAgentDetailEndpoint(CodexAgentEntitledEndpoint):
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def get(self, request, slug, agent_id):
entitlement_error = self.require_entitlement(request, slug)
if entitlement_error is not None:
@@ -185,7 +223,7 @@ class CodexAgentDetailEndpoint(CodexAgentEntitledEndpoint):
return gateway_request("GET", f"/api/internal/v1/owners/{owner_path(request.user)}/agents/{agent_path(agent_id)}")
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def patch(self, request, slug, agent_id):
entitlement_error = self.require_entitlement(request, slug)
if entitlement_error is not None:
@@ -200,7 +238,7 @@ class CodexAgentDetailEndpoint(CodexAgentEntitledEndpoint):
class CodexAgentRevokeEndpoint(CodexAgentEntitledEndpoint):
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def post(self, request, slug, agent_id):
entitlement_error = self.require_entitlement(request, slug)
if entitlement_error is not None:
@@ -214,7 +252,7 @@ class CodexAgentRevokeEndpoint(CodexAgentEntitledEndpoint):
class CodexAgentGrantListEndpoint(CodexAgentEntitledEndpoint):
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def get(self, request, slug, agent_id):
entitlement_error = self.require_entitlement(request, slug)
if entitlement_error is not None:
@@ -222,7 +260,7 @@ class CodexAgentGrantListEndpoint(CodexAgentEntitledEndpoint):
return gateway_request("GET", f"/api/internal/v1/owners/{owner_path(request.user)}/agents/{agent_path(agent_id)}/grants")
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def post(self, request, slug, agent_id):
entitlement_error = self.require_entitlement(request, slug)
if entitlement_error is not None:
@@ -233,7 +271,7 @@ class CodexAgentGrantListEndpoint(CodexAgentEntitledEndpoint):
return workspace_error
project_id = request.data.get("project_id")
project_error = validate_project_in_workspace(workspace, project_id)
project_error = validate_project_in_workspace(workspace, project_id, request.user)
if project_error is not None:
return project_error
@@ -247,7 +285,7 @@ class CodexAgentGrantListEndpoint(CodexAgentEntitledEndpoint):
class CodexAgentTokenListEndpoint(CodexAgentEntitledEndpoint):
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def get(self, request, slug, agent_id):
entitlement_error = self.require_entitlement(request, slug)
if entitlement_error is not None:
@@ -255,7 +293,7 @@ class CodexAgentTokenListEndpoint(CodexAgentEntitledEndpoint):
return gateway_request("GET", f"/api/internal/v1/owners/{owner_path(request.user)}/agents/{agent_path(agent_id)}/tokens")
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def post(self, request, slug, agent_id):
entitlement_error = self.require_entitlement(request, slug)
if entitlement_error is not None:
@@ -269,7 +307,7 @@ class CodexAgentTokenListEndpoint(CodexAgentEntitledEndpoint):
class CodexAgentTokenRevokeEndpoint(CodexAgentEntitledEndpoint):
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def post(self, request, slug, agent_id, token_id):
entitlement_error = self.require_entitlement(request, slug)
if entitlement_error is not None:
@@ -283,7 +321,7 @@ class CodexAgentTokenRevokeEndpoint(CodexAgentEntitledEndpoint):
class CodexAgentSetupEndpoint(CodexAgentEntitledEndpoint):
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def get(self, request, slug, agent_id):
entitlement_error = self.require_entitlement(request, slug)
if entitlement_error is not None: