-
Notifications
You must be signed in to change notification settings - Fork 49
fix(authz): grant legacy agent register principal #325
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -392,12 +392,13 @@ def _mocks(principal_context): | |
| return authorization, agents_use_case, api_keys_use_case | ||
|
|
||
| @staticmethod | ||
| def _request(): | ||
| def _request(principal_context=None): | ||
| return RegisterAgentRequest( | ||
| name="my-agent", | ||
| description="d", | ||
| acp_url="http://agent:5000", | ||
| acp_type=ACPType.ASYNC, | ||
| principal_context=principal_context, | ||
| ) | ||
|
|
||
| @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 | |
| use_case.register_agent.assert_awaited_once() | ||
| assert resp.agent_api_key == "internal-key" | ||
|
Comment on lines
404
to
415
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The parametrize covers
is never tested. With the new fallback logic, the body principal would be used here ( Consider adding a parametrized variant like: @pytest.mark.parametrize(
"auth_principal,body_principal",
[
({}, {"user_id": "u", "account_id": "acct"}),
({"account_id": "acct"}, {"user_id": "u", "account_id": "acct"}),
],
)
async def test_unresolvable_auth_service_with_resolvable_body_enforces(self, auth_principal, body_principal):
authz, use_case, api_keys = self._mocks(principal_context=auth_principal)
await register_agent(self._request(principal_context=body_principal), use_case, authz, api_keys)
authz.check.assert_awaited_once()
assert authz.check.await_args.kwargs["principal_context"] == body_principal
authz.grant.assert_awaited_once()
assert authz.grant.await_args.kwargs["principal_context"] == body_principalPrompt To Fix With AIThis is a comment left during a code review.
Path: agentex/tests/unit/api/test_agents_authz.py
Line: 404-415
Comment:
**Missing test: unresolvable non-None auth-service principal + resolvable body principal**
The parametrize covers `[None, {}, {"account_id":"acct"}]` for the auth-service principal, but always calls `self._request()` with no body `principal_context`. This means the case:
- auth service has `{"account_id": "acct"}` (unresolvable) **AND** body has `{"user_id": "u"}` (resolvable)
is never tested. With the new fallback logic, the body principal would be used here (`enforce_ownership=True`), which is the intended behavior — but there is zero coverage for it. A regression that skips enforcement in this case would pass all current tests.
Consider adding a parametrized variant like:
```python
@pytest.mark.parametrize(
"auth_principal,body_principal",
[
({}, {"user_id": "u", "account_id": "acct"}),
({"account_id": "acct"}, {"user_id": "u", "account_id": "acct"}),
],
)
async def test_unresolvable_auth_service_with_resolvable_body_enforces(self, auth_principal, body_principal):
authz, use_case, api_keys = self._mocks(principal_context=auth_principal)
await register_agent(self._request(principal_context=body_principal), use_case, authz, api_keys)
authz.check.assert_awaited_once()
assert authz.check.await_args.kwargs["principal_context"] == body_principal
authz.grant.assert_awaited_once()
assert authz.grant.await_args.kwargs["principal_context"] == body_principal
```
How can I resolve this? If you propose a fix, please make it concise. |
||
|
|
||
| async def test_body_principal_enforces_check_and_grant(self): | ||
| # Legacy deployed pods send the deploy principal in the body because | ||
| # /agents/register is whitelisted and has no request-state principal. | ||
| body_principal = {"user_id": "u", "account_id": "acct"} | ||
| authz, use_case, api_keys = self._mocks(principal_context=None) | ||
|
|
||
| await register_agent( | ||
| self._request(principal_context=body_principal), use_case, authz, api_keys | ||
| ) | ||
|
|
||
| authz.check.assert_awaited_once() | ||
| assert authz.check.await_args.kwargs["principal_context"] == body_principal | ||
| authz.grant.assert_awaited_once() | ||
| assert authz.grant.await_args.kwargs["principal_context"] == body_principal | ||
|
|
||
| async def test_dict_principal_enforces_check_and_grant(self): | ||
| authz, use_case, api_keys = self._mocks( | ||
| principal_context={"user_id": "u", "account_id": "acct"} | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/agents/registeris inWHITELISTED_ROUTES, so the auth middleware setsrequest.state.principal_context = Noneand skips all credential validation for every request to this path — including requests from external, untrusted callers.With this PR, any caller to the endpoint can supply:
{"principal_context": {"user_id": "any-user-id", "account_id": "any-account"}}_has_resolvable_creatorwill returnTruefor that dict,enforce_ownershipbecomesTrue, and the code proceeds to callauthorization_service.grant(AgentexResource.agent(...), principal_context=<attacker_dict>). TheAgentexAuthorizationProxy.grantthen forwards this dict verbatim toagentex-auth /v1/authz/grantwith no cryptographic proof that the caller is actually that user.Whether this results in a successful ownership escalation depends on whether agentex-auth performs independent principal validation server-side. If it accepts the principal dict at face value (as a trusted internal call), any caller who can reach this endpoint can claim ownership of agents under any identity.
Recommendation: If the body-principal trust model depends on network isolation (only internal pods can reach this endpoint), document that assumption explicitly. Consider adding a comment on the
principal_contextfield and here explaining the trust model, and/or adding a check that explicitly limits this fallback to only whenAGENTEX_AUTH_URLis configured in a way that signals internal-only trust.Prompt To Fix With AI