fix(agents): reject whitespace-only delegated scope claims

This commit is contained in:
Joshua Valluru 2026-09-26 12:47:35 -07:00
parent 928df912c0
commit b855ecd187
3 changed files with 8 additions and 4 deletions

View file

@ -32,7 +32,8 @@ def classify_agent_subject(
if isinstance(scope, str) and scope:
if allowed_mode == "autonomous" or oid == binding.service_principal_id or claims.get("idtyp") == "app":
return AgentIdentityFailure(message="Delegated token contradicts the configured agent identity or mode")
if not frozenset(binding.required_scopes).issubset(scope.split()):
granted_scopes: Final = frozenset(scope.split())
if not granted_scopes or not frozenset(binding.required_scopes).issubset(granted_scopes):
return AgentIdentityFailure(message="Token lacks the required delegated scopes")
return AgentSubject(kind="delegated_subject", oid=oid, mode="delegated")
if allowed_mode == "delegated" or oid != binding.service_principal_id or claims.get("idtyp") == "user":

View file

@ -2,7 +2,7 @@ from datetime import datetime
from typing import Literal, TypeAlias
from uuid import UUID
from pydantic import BaseModel, ConfigDict, field_validator
from pydantic import BaseModel, ConfigDict, Field, field_validator
AgentExecutionMode: TypeAlias = Literal["autonomous", "delegated", "both"]
@ -15,7 +15,10 @@ class EntraIdentityConfig(BaseModel):
client_id: str
service_principal_id: str | None = None
required_roles: tuple[str, ...] = ()
required_scopes: tuple[str, ...] = ("user_impersonation",)
required_scopes: tuple[str, ...] = Field(
default=("user_impersonation",),
description="Required delegated scopes. An empty list accepts any nonempty scope granted for this gateway.",
)
@field_validator("tenant_id", "client_id", "service_principal_id")
@classmethod

View file

@ -121,7 +121,7 @@ def test_empty_required_scopes_allow_valid_delegated_scope(mode: AgentExecutionM
assert result == AgentSubject(kind="delegated_subject", oid=HUMAN, mode="delegated")
@pytest.mark.parametrize("scope", [None, "", 42])
@pytest.mark.parametrize("scope", [None, "", " \t ", 42])
def test_empty_requirements_do_not_make_a_scope_less_human_token_valid(scope: object) -> None:
binding: Final = BINDING.model_copy(update={"required_scopes": ()})
result: Final = classify_agent_subject(binding, claims(oid=HUMAN, scp=scope), "both")