Skip to content

Commit cae5f94

Browse files
authored
fix(authz): grant legacy agent register principal (#325)
## Summary - Restore legacy `/agents/register` compatibility by accepting `principal_context` in the register request body. - Use the request-state principal when present, otherwise fall back to the body principal for create-check and ownership grant. - Keep the #292 behavior where register skips authz when no resolvable principal is provided, avoiding the unauthenticated self-register crashloop. - Regenerate the Agentex OpenAPI spec. ## Validation - `uv run pytest agentex/tests/unit/api/test_agents_authz.py -q` - `uv run ruff check agentex/src/api/routes/agents.py agentex/src/api/schemas/agents.py agentex/tests/unit/api/test_agents_authz.py` - `uv run ruff format --check agentex/src/api/routes/agents.py agentex/src/api/schemas/agents.py agentex/tests/unit/api/test_agents_authz.py` - `make gen-openapi`
1 parent a51ed7a commit cae5f94

4 files changed

Lines changed: 32 additions & 2 deletions

File tree

agentex/openapi.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5221,6 +5221,12 @@ components:
52215221
acp_type:
52225222
$ref: '#/components/schemas/ACPType'
52235223
description: The type of ACP to use for the agent.
5224+
principal_context:
5225+
anyOf:
5226+
- {}
5227+
- type: 'null'
5228+
title: Principal Context
5229+
description: Principal used for authorization
52245230
registration_metadata:
52255231
anyOf:
52265232
- additionalProperties: true

agentex/src/api/routes/agents.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,11 +195,15 @@ async def register_agent(
195195
If agent_id is not provided, the system will look for an existing agent by name and update it,
196196
or create a new one if it doesn't exist.
197197
"""
198-
enforce_ownership = _has_resolvable_creator(authorization_service.principal_context)
198+
principal_context = authorization_service.principal_context
199+
if not _has_resolvable_creator(principal_context):
200+
principal_context = request.principal_context
201+
enforce_ownership = _has_resolvable_creator(principal_context)
199202
if enforce_ownership:
200203
await authorization_service.check(
201204
AgentexResource.agent("*"),
202205
AuthorizedOperationType.create,
206+
principal_context=principal_context,
203207
)
204208
logger.info(
205209
"Registering agent name=%s agent_id=%s acp_type=%s",
@@ -220,6 +224,7 @@ async def register_agent(
220224
if enforce_ownership:
221225
await authorization_service.grant(
222226
AgentexResource.agent(agent_entity.id),
227+
principal_context=principal_context,
223228
)
224229
response_fields = agent_entity.model_dump()
225230
existing_key = await api_keys_use_case.get_internal_api_key_by_agent_id(

agentex/src/api/schemas/agents.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ class RegisterAgentRequest(BaseModel):
8888
description="Optional agent ID if the agent already exists and needs to be updated.",
8989
)
9090
acp_type: ACPType = Field(..., description="The type of ACP to use for the agent.")
91+
principal_context: Any | None = Field(
92+
default=None, description="Principal used for authorization"
93+
)
9194
registration_metadata: dict[str, Any] | None = Field(
9295
default=None,
9396
description="The metadata for the agent's registration.",

agentex/tests/unit/api/test_agents_authz.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,12 +392,13 @@ def _mocks(principal_context):
392392
return authorization, agents_use_case, api_keys_use_case
393393

394394
@staticmethod
395-
def _request():
395+
def _request(principal_context=None):
396396
return RegisterAgentRequest(
397397
name="my-agent",
398398
description="d",
399399
acp_url="http://agent:5000",
400400
acp_type=ACPType.ASYNC,
401+
principal_context=principal_context,
401402
)
402403

403404
@pytest.mark.parametrize("principal_context", [None, {}, {"account_id": "acct"}])
@@ -413,6 +414,21 @@ async def test_unresolvable_creator_skips_check_and_grant(self, principal_contex
413414
use_case.register_agent.assert_awaited_once()
414415
assert resp.agent_api_key == "internal-key"
415416

417+
async def test_body_principal_enforces_check_and_grant(self):
418+
# Legacy deployed pods send the deploy principal in the body because
419+
# /agents/register is whitelisted and has no request-state principal.
420+
body_principal = {"user_id": "u", "account_id": "acct"}
421+
authz, use_case, api_keys = self._mocks(principal_context=None)
422+
423+
await register_agent(
424+
self._request(principal_context=body_principal), use_case, authz, api_keys
425+
)
426+
427+
authz.check.assert_awaited_once()
428+
assert authz.check.await_args.kwargs["principal_context"] == body_principal
429+
authz.grant.assert_awaited_once()
430+
assert authz.grant.await_args.kwargs["principal_context"] == body_principal
431+
416432
async def test_dict_principal_enforces_check_and_grant(self):
417433
authz, use_case, api_keys = self._mocks(
418434
principal_context={"user_id": "u", "account_id": "acct"}

0 commit comments

Comments
 (0)