fix(api-tools): forbid processors from widening workspace operation grants

A processor granted pipeline.extensions.update / pipeline.update / agent.update
derives resource.manage, which let it add any resource.manage operation to its
own grants. Granting is a human authorization act, so processor principals may
only keep or remove grants, and the catalog marks nothing grantable for them.

LANG-226

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
This commit is contained in:
RockChinQ
2026-11-01 12:03:54 +08:00
co-authored by Claude Opus 5.5 multica-agent
parent a0405a0b2c
commit 24038395dd
2 changed files with 49 additions and 1 deletions
+18 -1
View File
@@ -10,6 +10,11 @@ from .contracts import ApplicationAPIError, ApplicationAPIException, Operation
from .operation_docs import DOMAINS, get_operation_doc
def _is_processor(context: Any) -> bool:
principal = getattr(context, 'principal', None)
return getattr(principal, 'principal_type', None) == PrincipalType.PROCESSOR
def validate_grant_change(
ap: Any,
editor_context: RequestContext,
@@ -66,6 +71,18 @@ def validate_grant_change(
if not added_ids:
return normalized_new
# Granting is a human authorization act. A processor that can update processor
# configuration must not widen any processor's grants, including its own.
if _is_processor(editor_context):
raise ApplicationAPIException(
ApplicationAPIError(
code='permission_denied',
message=f'Processors cannot grant workspace operations: {added_ids}',
status_code=403,
details={'operations': added_ids, 'denied_operations': added_ids},
)
)
app_api = getattr(ap, 'application_api', None)
if app_api is None:
from .api import ApplicationAPI
@@ -147,7 +164,7 @@ def get_workspace_operations_catalog(ap: Any, context: Any) -> dict[str, Any]:
has_perm = (
has_permission(context, spec.permission) if context is not None and hasattr(context, 'workspace') else False
)
grantable = bool(can_run and has_perm)
grantable = bool(can_run and has_perm and not _is_processor(context))
perm = spec.permission
perm_str = perm.value if hasattr(perm, 'value') else str(perm)
+31
View File
@@ -249,3 +249,34 @@ def test_workspace_operations_catalog_and_grantable_flags(test_host):
]
for op in viewer_view_ops:
assert op['grantable'] is True
# 8. A processor must not widen grants, even within its own derived permissions
def test_processor_principal_cannot_add_grants(test_host):
import asyncio
from langbot.pkg.api.application.processor_context import build_processor_request_context
processor_ctx = asyncio.run(
build_processor_request_context(
test_host,
workspace_uuid='ws-1',
processor_kind='pipeline',
processor_uuid='pipe-1',
run_id='run-1',
operation_ids=['pipeline.extensions.update'],
)
)
old = ['pipeline.extensions.update']
with pytest.raises(ApplicationAPIException) as exc_info:
validate_grant_change(test_host, processor_ctx, old, [*old, 'bot.delete'])
assert exc_info.value.error.code == 'permission_denied'
assert exc_info.value.error.details['operations'] == ['bot.delete']
# Keeping or removing existing grants is still allowed.
assert validate_grant_change(test_host, processor_ctx, old, old) == old
assert validate_grant_change(test_host, processor_ctx, old, []) == []
catalog = get_workspace_operations_catalog(test_host, processor_ctx)
assert not any(op['grantable'] for op in catalog['workspace_operations'])