hataa 92c8f2f03b
feat(authz): add built-in RBAC provider and provider factory (#4260)
* feat(authz): add built-in RBAC provider and provider factory (Phase 1A-2, #4063)

Phase 1A-2: RBAC provider + provider factory. No runtime behavior change.

New authz/rbac.py — RbacAuthorizationProvider:
- allow: '*' / True / list / [] / missing → deny-wins semantics
- deny always overrides all allow forms
- resource name explicit mapping (tool→tools, model→models, etc.)
- unknown/missing role raises ValueError (never silent allow)
- config compiled to immutable frozensets at construction
- filter_resources preserves order, no mutation, consistent with authorize
- sync == async decisions

New authz/runtime.py — resolve_authorization_provider:
- disabled → None (no import attempted)
- enabled + no provider → ValueError
- invalid class path / construction failure → ValueError with path
- isinstance Protocol check post-construction
- no caching, no fail_closed/default_role injection

48 tests (37 RBAC + 11 factory). No config schema change, no config_version bump.
Per RFC #4063 Phase 1A-2. Layer 1/Layer 2 wiring deferred to Phase 1B.

* fix(authz): reject unknown RBAC provider config

Fail fast on misspelled top-level RBAC settings, cover factory error propagation, and record Phase 1B policy and audit caveats.

* fix(authz): reject unreachable resource aliases

Fail fast when RBAC config uses reserved request-side aliases, preserve same-name and custom resources, and add regression coverage for every mapped alias.

* fix(authz): validate RBAC request identifiers
2026-07-21 09:23:14 +08:00

51 lines
2.0 KiB
Python

"""Provider factory — resolves and constructs the configured AuthorizationProvider.
This is the single entry point for creating an authorization provider from
``AuthorizationConfig``. It does not cache instances (Phase 1B resolves once
per agent build and passes the same instance to Layer 1 and Layer 2).
"""
from __future__ import annotations
from deerflow.authz.provider import AuthorizationProvider
from deerflow.config.authorization_config import AuthorizationConfig
from deerflow.reflection import resolve_variable
def resolve_authorization_provider(
config: AuthorizationConfig,
) -> AuthorizationProvider | None:
"""Resolve the authorization provider from config.
Returns:
A constructed ``AuthorizationProvider`` instance, or ``None`` if
authorization is disabled.
Raises:
ValueError: If ``enabled`` is True but no provider is configured,
or if the class path is invalid / construction fails / the
instance does not satisfy the ``AuthorizationProvider`` Protocol.
"""
if not config.enabled:
return None
if config.provider is None:
raise ValueError("authorization.enabled is true but no provider is configured; set authorization.provider.use to a class path")
class_path = config.provider.use
try:
provider_cls = resolve_variable(class_path, expected_type=type)
except (ImportError, ValueError) as err:
raise ValueError(f"Failed to resolve authorization provider class '{class_path}': {err}") from err
kwargs = dict(config.provider.config) if config.provider.config else {}
try:
instance = provider_cls(**kwargs)
except Exception as err:
raise ValueError(f"Failed to construct authorization provider '{class_path}': {err}") from err
if not isinstance(instance, AuthorizationProvider):
raise ValueError(f"Authorization provider '{class_path}' does not satisfy the AuthorizationProvider Protocol")
return instance