mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-27 08:28:00 +00:00
* feat(authz): enforce tool authorization at assembly and runtime * fix(middleware): guard deferred tool setup lookup (#4370) --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
139 lines
5.9 KiB
Python
139 lines
5.9 KiB
Python
"""Adapter that presents an AuthorizationProvider as a GuardrailProvider.
|
|
|
|
This lets the existing :class:`~deerflow.guardrails.middleware.GuardrailMiddleware`
|
|
enforce :class:`~deerflow.authz.provider.AuthorizationProvider` decisions at
|
|
tool-call time — no new middleware class required (see RFC §6.1).
|
|
|
|
The adapter maps :class:`~deerflow.guardrails.provider.GuardrailRequest`
|
|
fields to :class:`~deerflow.authz.provider.AuthzRequest` fields, calls the
|
|
authorization provider, and converts the :class:`~deerflow.authz.provider.AuthzDecision`
|
|
back to a :class:`~deerflow.guardrails.provider.GuardrailDecision`.
|
|
|
|
Principal construction delegates to
|
|
:func:`~deerflow.authz.principal.build_principal_from_context` so Layer 1
|
|
(tool assembly) and Layer 2 (this adapter) share a single identity builder
|
|
with consistent ``default_role`` and ``attributes`` semantics.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Iterable
|
|
|
|
from deerflow.authz.principal import build_principal_from_context
|
|
from deerflow.authz.provider import AuthorizationProvider, AuthzDecision, AuthzRequest
|
|
from deerflow.guardrails.provider import GuardrailDecision, GuardrailReason, GuardrailRequest
|
|
|
|
|
|
class GuardrailAuthorizationAdapter:
|
|
"""Adapt an :class:`AuthorizationProvider` to the ``GuardrailProvider`` Protocol.
|
|
|
|
``resource_type`` and ``action`` default to ``"tool"`` / ``"call"``,
|
|
which is correct for the tool-execution path. A different resource/action
|
|
pair can be injected if the adapter is reused outside the tool path.
|
|
|
|
Args:
|
|
provider: The authorization provider to delegate decisions to.
|
|
default_role: Role used when ``user_role`` is absent or empty in the
|
|
runtime context. Must be passed by Phase 1B wiring from
|
|
``AuthorizationConfig.default_role``.
|
|
resource_type: Resource type for all ``AuthzRequest`` instances.
|
|
action: Action for all ``AuthzRequest`` instances.
|
|
infrastructure_tool_names: Framework tools created from an already
|
|
authorized capability set. These may execute without a second
|
|
provider decision; callers must derive the names from the current
|
|
build's concrete deferred setup rather than from static config.
|
|
"""
|
|
|
|
name = "authorization"
|
|
|
|
def __init__(
|
|
self,
|
|
provider: AuthorizationProvider,
|
|
*,
|
|
default_role: str = "user",
|
|
resource_type: str = "tool",
|
|
action: str = "call",
|
|
infrastructure_tool_names: Iterable[str] = (),
|
|
) -> None:
|
|
self._provider = provider
|
|
self._default_role = default_role
|
|
self._resource_type = resource_type
|
|
self._action = action
|
|
self._infrastructure_tool_names = frozenset(infrastructure_tool_names)
|
|
|
|
def _infrastructure_decision(self, request: GuardrailRequest) -> GuardrailDecision | None:
|
|
"""Allow framework tools created from an already-filtered capability set."""
|
|
if request.tool_name not in self._infrastructure_tool_names:
|
|
return None
|
|
return GuardrailDecision(
|
|
allow=True,
|
|
reasons=[GuardrailReason(code="authz.infrastructure_tool")],
|
|
policy_id="authz:infrastructure",
|
|
)
|
|
|
|
def _to_authz(self, gr: GuardrailRequest) -> AuthzRequest:
|
|
"""Map a guardrail request to an authorization request."""
|
|
principal = build_principal_from_context(
|
|
{
|
|
"user_id": gr.user_id,
|
|
"user_role": gr.user_role,
|
|
"oauth_provider": gr.oauth_provider,
|
|
"oauth_id": gr.oauth_id,
|
|
"channel_user_id": gr.channel_user_id,
|
|
"is_internal": gr.is_internal,
|
|
"authz_attributes": gr.authz_attributes,
|
|
},
|
|
default_role=self._default_role,
|
|
)
|
|
return AuthzRequest(
|
|
principal=principal,
|
|
resource=self._resource_type,
|
|
action=self._action,
|
|
target=gr.tool_name,
|
|
context={
|
|
"thread_id": gr.thread_id,
|
|
"run_id": gr.run_id,
|
|
"tool_call_id": gr.tool_call_id,
|
|
"tool_input": gr.tool_input,
|
|
"is_subagent": gr.is_subagent,
|
|
"agent_id": gr.agent_id,
|
|
"timestamp": gr.timestamp,
|
|
},
|
|
)
|
|
|
|
@staticmethod
|
|
def _to_guardrail(d: AuthzDecision) -> GuardrailDecision:
|
|
"""Convert an authorization decision to a guardrail decision."""
|
|
return GuardrailDecision(
|
|
allow=d.allow,
|
|
reasons=[GuardrailReason(code=r.code, message=r.message) for r in d.reasons],
|
|
policy_id=d.policy_id,
|
|
metadata=d.metadata,
|
|
)
|
|
|
|
def evaluate(self, request: GuardrailRequest) -> GuardrailDecision:
|
|
"""Synchronous evaluation: delegate to ``provider.authorize``.
|
|
|
|
Provider exceptions are intentionally allowed to propagate. The
|
|
adapter is consumed by :class:`~deerflow.guardrails.middleware.GuardrailMiddleware`,
|
|
whose ``wrap_tool_call`` / ``awrap_tool_call`` already applies
|
|
fail-closed semantics based on its ``fail_closed`` parameter
|
|
(backed by ``AuthorizationConfig.fail_closed``). Catching exceptions
|
|
here would duplicate that logic and risk divergent behavior between
|
|
the two layers.
|
|
"""
|
|
if infrastructure_decision := self._infrastructure_decision(request):
|
|
return infrastructure_decision
|
|
decision = self._provider.authorize(self._to_authz(request))
|
|
return self._to_guardrail(decision)
|
|
|
|
async def aevaluate(self, request: GuardrailRequest) -> GuardrailDecision:
|
|
"""Async evaluation: delegate to ``provider.aauthorize``.
|
|
|
|
See :meth:`evaluate` for exception-propagation rationale.
|
|
"""
|
|
if infrastructure_decision := self._infrastructure_decision(request):
|
|
return infrastructure_decision
|
|
decision = await self._provider.aauthorize(self._to_authz(request))
|
|
return self._to_guardrail(decision)
|