* feat(authz): enforce model authorization at Gateway routes and runtime (#4063 Phase 3)
Phase 3 / Models — the first of three resource-type PRs (Models, Skills,
Sandbox). The RBAC provider already maps "model" → config key "models"
(rbac.py _RESOURCE_POLICY_KEYS), so no schema change is needed.
Gateway route layer (mirrors Phase 2A):
- resolve_model_authorization() in authz.py returns (provider, principal),
reusing _get_cached_route_provider and build_principal_from_context,
including the INTERNAL_SYSTEM_ROLE → None pop for internal callers.
- list_models filters via provider.filter_resources(principal, "model", names).
- get_model checks provider.authorize("model", "use"). Deny → 403 (not 404,
since the model exists but the role lacks permission).
Runtime resolution layer (mirrors Phase 1B):
- _authorize_model_name() in agent.py runs after _resolve_model_name. On deny,
falls back to the first allowed model (RFC §9: graceful, not crash). All
models denied + fail_closed → ValueError (matches existing contract).
authorization.enabled: false is a complete no-op on both layers. Anonymous
requests (user=None) bypass filtering. 18 new tests + 314 existing tests pass.
* fix(authz): enforce model:use on the embedded DeerFlowClient path (Phase 3 follow-up)
Round 4 review (willem-bd): _authorize_model_name only covered the Gateway
runtime path (_make_lead_agent). The parallel lead-agent construction path
DeerFlowClient._ensure_agent (client.py) filtered tools but not the model,
so a library/embedded consumer with role-scoped model policies could run a
model the role is denied model:use for.
- Insert _authorize_model_name in _ensure_agent, mirroring _make_lead_agent.
- Resolve None default to the first configured model before the gate so the
implicit default (create_chat_model(name=None)) is also authorized.
- Update test_authorization_filters_framework_tools_and_reuses_provider: the
stub provider now returns an allow decision for model:use (checked during
assembly) and patches resolve_authorization_provider in the agent namespace.
- Add 3 DeerFlowClient._ensure_agent path tests (real-path fallback,
None-default resolution, disabled no-op); 24 tests total.
* docs(authz): document get_model provider-unavailable fail-open path + test
zhfeng review (round 5): get_model's docstring only mentioned the deny→403
path, not the provider-resolution-error + fail-open path (which allows the
request, mirroring list_models's documented fail-open semantics). The
behavior itself is correct and symmetric with list_models, but it was
undocumented and the _AuthorizationUnavailable path had no test coverage.
- Extend get_model docstring to state the provider-error fail-closed/fail-open
outcome, matching list_models's wording.
- Add test_get_model_provider_unavailable_fail_closed_vs_open exercising the
_AuthorizationUnavailable path (provider cannot be resolved at all), pinning
fail-closed→403 / fail-open→200.
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>