mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-11-01 05:15:22 +08:00
feat(api-tools): support processor principal and operation whitelist enforcement
LANG-230 Co-authored-by: multica-agent <[email protected]>
This commit is contained in:
co-authored by
multica-agent
parent
8f2f537e34
commit
c123a0da79
@@ -10,7 +10,7 @@ import logging
|
||||
|
||||
from pydantic import TypeAdapter, ValidationError
|
||||
|
||||
from ..authz import AuthenticationDeniedError, require_permission
|
||||
from ..authz import AuthenticationDeniedError, PermissionDeniedError, require_permission
|
||||
from ..context import PrincipalType, RequestContext
|
||||
from ...i18n import language_scope
|
||||
from ...i18n.workspace import get_workspace_language
|
||||
@@ -67,10 +67,15 @@ class ApplicationAPI:
|
||||
PrincipalType.ACCOUNT,
|
||||
PrincipalType.API_KEY,
|
||||
PrincipalType.SUPPORT_ADMIN,
|
||||
PrincipalType.PROCESSOR,
|
||||
}:
|
||||
raise AuthenticationDeniedError('This principal cannot invoke Workspace management operations')
|
||||
if spec.principals is not None and context.principal.principal_type not in spec.principals:
|
||||
raise AuthenticationDeniedError('This operation requires a different principal')
|
||||
if context.principal.principal_type == PrincipalType.PROCESSOR and context.allowed_operation_ids is None:
|
||||
raise PermissionDeniedError('allowed_operation_ids')
|
||||
if context.allowed_operation_ids is not None and spec.operation_id not in context.allowed_operation_ids:
|
||||
raise PermissionDeniedError(spec.operation_id)
|
||||
require_permission(context, spec.permission)
|
||||
try:
|
||||
payload = spec.input_type.model_validate(input)
|
||||
@@ -183,6 +188,7 @@ class ApplicationAPI:
|
||||
PrincipalType.ACCOUNT,
|
||||
PrincipalType.API_KEY,
|
||||
PrincipalType.SUPPORT_ADMIN,
|
||||
PrincipalType.PROCESSOR,
|
||||
}
|
||||
):
|
||||
return
|
||||
@@ -191,6 +197,8 @@ class ApplicationAPI:
|
||||
if context.principal.principal_type == PrincipalType.API_KEY
|
||||
else context.principal.actor_account_uuid
|
||||
if context.principal.principal_type == PrincipalType.SUPPORT_ADMIN
|
||||
else context.principal.processor_uuid
|
||||
if context.principal.principal_type == PrincipalType.PROCESSOR
|
||||
else context.account_uuid
|
||||
)
|
||||
if not isinstance(principal_id, str) or not principal_id.strip():
|
||||
@@ -255,11 +263,29 @@ class ApplicationAPI:
|
||||
break
|
||||
if resource_id is None and identity:
|
||||
resource_id = next(iter(identity.values()))
|
||||
detail = {'operation': spec.operation_id, 'version': spec.version}
|
||||
actor_name = None
|
||||
if context.principal.principal_type == PrincipalType.PROCESSOR:
|
||||
actor_name = (
|
||||
f'{context.principal.processor_kind}:{context.principal.processor_uuid}'
|
||||
if context.principal.processor_kind
|
||||
else context.principal.processor_uuid
|
||||
)
|
||||
detail.update(
|
||||
{
|
||||
'processor_kind': context.principal.processor_kind,
|
||||
'processor_uuid': context.principal.processor_uuid,
|
||||
'run_id': context.principal.run_id,
|
||||
}
|
||||
)
|
||||
if context.principal.trigger_actor:
|
||||
detail['trigger_actor'] = context.principal.trigger_actor
|
||||
await service.record(
|
||||
context.workspace_uuid,
|
||||
rule=ActionRule(action, 'resource', bucket, parts[0]),
|
||||
level=level,
|
||||
actor_account_uuid=context.principal.actor_account_uuid or context.account_uuid,
|
||||
actor_name=actor_name,
|
||||
actor_role=context.workspace.role,
|
||||
principal_type=context.principal.principal_type.value,
|
||||
api_key_uuid=context.principal.api_key_uuid,
|
||||
@@ -269,7 +295,7 @@ class ApplicationAPI:
|
||||
resource_id=resource_id,
|
||||
outcome=outcome,
|
||||
status_code=status,
|
||||
detail={'operation': spec.operation_id, 'version': spec.version},
|
||||
detail=detail,
|
||||
duration_ms=int((time.monotonic() - observed_at) * 1000),
|
||||
)
|
||||
except Exception:
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Processor authorization context factory for ApplicationAPI invocation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
import uuid
|
||||
from collections.abc import Iterable
|
||||
|
||||
from ..context import PrincipalContext, PrincipalType, RequestContext, WorkspaceContext
|
||||
|
||||
|
||||
async def build_processor_request_context(
|
||||
ap: typing.Any,
|
||||
*,
|
||||
workspace_uuid: str,
|
||||
processor_kind: str,
|
||||
processor_uuid: str,
|
||||
run_id: str,
|
||||
operation_ids: Iterable[str],
|
||||
trigger_actor: str | None = None,
|
||||
) -> RequestContext:
|
||||
"""Build a trusted RequestContext for internal processor invocations."""
|
||||
workspace_service = getattr(ap, 'workspace_service', None)
|
||||
if workspace_service is None and hasattr(ap, 'ap'):
|
||||
workspace_service = getattr(ap.ap, 'workspace_service', None)
|
||||
if workspace_service is None:
|
||||
raise RuntimeError('Workspace service is required to resolve execution binding')
|
||||
|
||||
binding = await workspace_service.get_execution_binding(workspace_uuid)
|
||||
|
||||
app_api = getattr(ap, 'application_api', None)
|
||||
if app_api is None and hasattr(ap, '_operations'):
|
||||
app_api = ap
|
||||
if app_api is None and hasattr(ap, 'ap'):
|
||||
app_api = getattr(ap.ap, 'application_api', None)
|
||||
|
||||
allowed_ids = frozenset(operation_ids)
|
||||
permissions: set[str] = set()
|
||||
if app_api is not None:
|
||||
operations = getattr(app_api, '_operations', {})
|
||||
for op_id in allowed_ids:
|
||||
spec = operations.get(op_id)
|
||||
if spec is not None and getattr(spec, 'permission', None) is not None:
|
||||
perm = spec.permission
|
||||
permissions.add(perm.value if hasattr(perm, 'value') else str(perm))
|
||||
|
||||
principal = PrincipalContext(
|
||||
principal_type=PrincipalType.PROCESSOR,
|
||||
account_uuid=None,
|
||||
actor_account_uuid=None,
|
||||
api_key_uuid=None,
|
||||
support_session_id=None,
|
||||
processor_kind=processor_kind,
|
||||
processor_uuid=processor_uuid,
|
||||
run_id=run_id,
|
||||
trigger_actor=trigger_actor,
|
||||
)
|
||||
workspace = WorkspaceContext(
|
||||
workspace_uuid=workspace_uuid,
|
||||
membership_uuid=None,
|
||||
role=None,
|
||||
permissions=frozenset(permissions),
|
||||
membership_revision=0,
|
||||
)
|
||||
return RequestContext(
|
||||
instance_uuid=binding.instance_uuid,
|
||||
placement_generation=binding.placement_generation,
|
||||
request_id=str(uuid.uuid4()),
|
||||
auth_type='processor',
|
||||
principal=principal,
|
||||
workspace=workspace,
|
||||
entitlement_revision=0,
|
||||
allowed_operation_ids=allowed_ids,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ['build_processor_request_context']
|
||||
@@ -19,6 +19,7 @@ class PrincipalType(enum.StrEnum):
|
||||
SUPPORT_ADMIN = 'support_admin'
|
||||
SYSTEM = 'system'
|
||||
PUBLIC_BOT = 'public_bot'
|
||||
PROCESSOR = 'processor'
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True, slots=True)
|
||||
@@ -30,6 +31,10 @@ class PrincipalContext:
|
||||
actor_account_uuid: str | None = None
|
||||
api_key_uuid: str | None = None
|
||||
support_session_id: str | None = None
|
||||
processor_kind: str | None = None
|
||||
processor_uuid: str | None = None
|
||||
run_id: str | None = None
|
||||
trigger_actor: str | None = None
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True, slots=True)
|
||||
@@ -54,6 +59,7 @@ class RequestContext:
|
||||
principal: PrincipalContext
|
||||
workspace: WorkspaceContext
|
||||
entitlement_revision: int = 0
|
||||
allowed_operation_ids: frozenset[str] | None = None
|
||||
|
||||
@property
|
||||
def workspace_uuid(self) -> str:
|
||||
|
||||
Reference in New Issue
Block a user