Skip to content

Let a superuser issue a personal token for another user - #1117

Merged
cigamit merged 1 commit into
ctrliq:mainfrom
quanah:issue1116
Oct 14, 2026
Merged

cigamit merged 1 commit into
ctrliq:mainfrom
quanah:issue1116

Conversation

@quanah

@quanah quanah commented Oct 14, 2026

Copy link
Copy Markdown
Contributor
SUMMARY

Assisted-by: Claude Opus 5.5. Drafted with AI assistance; the commit carries the same trailer.

POST /api/v2/users/<id>/personal_tokens/ ignored <id>: every token serializer set the owner to the requester, so an admin got 201 and a token of their own. GET on the same URL listed the requester's tokens. That left no API path to give a token to a user who can't log in, such as a service account created without a password (#1090); the workaround was awx-manage create_oauth2_token on the controller.

With this change:

  • The token belongs to <id>. Users still create tokens for themselves; a superuser can create one for any user; anyone else gets 403.
  • For someone else, a superuser gets 400 if the owner is inactive, or is a local user while DISABLE_LOCAL_AUTH is on (turning that on revokes those users' tokens, so issuing new ones would undo it). The ALLOW_OAUTH2_FOR_EXTERNAL_USERS check applies to the owner.
  • GET lists <id>'s personal tokens, filtered by what the requester may read under OAuth2TokenAccess.
  • The activity-stream entry for a token whose owner isn't the actor names both: actor is the requester, changes.user is the owner ("svc-ci (7)"), and the entry is linked to the owner so it shows in their activity stream.
  • Docs: the /api/v2/users/1/personal_tokens/ example in docs/auth/oauth.md becomes <user_id>, and both OAuth docs say who may create a token for whom.

Design: superusers only, rather than anyone who may change the user. OAuth2TokenAccess already limits reading and revoking personal tokens to the owner and superusers; letting org admins issue them would leave them unable to list or revoke what they issued. Widening that is one condition in OAuth2TokenAccess.can_add, plus the matching read/delete rules, if you want it.

Compatibility: this changes what an existing endpoint returns. A superuser posting to another id now gets a token for that user, not themselves, and the response's user shows which. A non-superuser posting to another visible id gets 403 instead of 201; a superuser posting to a nonexistent id gets 404 instead of 201. Because the sub-list permission check asks can_add about the user in the URL, a non-superuser's POST to another user's tokens/ or authorized_tokens/ also gets 403 now; superusers there still get their own token. /api/v2/tokens/, the UI and ctrliq.ascender are unaffected, and the OpenAPI schema is unchanged. Details and the before/after table are in the issue.

related #1116

ISSUE TYPE
  • New or Enhanced Feature
COMPONENT NAME
  • API
  • Docs
ASCENDER VERSION
awx: 25.6.2-241-gc5da6e18
ADDITIONAL INFORMATION

Tests, in ascender/main/tests/functional/api/test_oauth.py:

  • test_superuser_issues_personal_token_for_another_user: 201, owner is the target, and the token authenticates as the target.
  • test_personal_token_for_another_user_needs_superuser: a normal user, an org admin of the target's organization and a system auditor all get 403, and no token is created.
  • test_personal_token_for_missing_user (404), test_personal_token_for_another_external_user (403 when ALLOW_OAUTH2_FOR_EXTERNAL_USERS is off), test_personal_token_for_another_inactive_user (400), test_personal_token_for_another_user_with_local_auth_disabled (400 for a local user, 201 for an LDAP one).
  • test_personal_token_for_self: unchanged for an admin and a normal user, through both users/<id>/personal_tokens/ and /api/v2/tokens/.
  • test_other_token_sub_urls_still_issue_to_the_requester: tokens/ and authorized_tokens/ give an org admin 403 for a member and a superuser a token of their own.
  • test_personal_token_list_is_the_url_users: the target's tokens for superusers, the owner and system auditors, an empty list for another user who can see the target, 403 for one who can't; application tokens excluded.
  • test_personal_token_for_another_user_activity_stream: actor, changes.user and the owner link for a token issued to someone else, nothing extra for one the actor issues to themselves.

Run locally in python:3.14 beside postgres:15, as CI does:

# the new tests, main + only the test changes
$ pytest ascender/main/tests/functional/api/test_oauth.py -k 'personal_token or superuser_issues or other_token_sub_urls'
11 failed, 3 passed          # the 3 that pass: the self path (2) and a normal user who can't see the target

# with this change
14 passed

# test_oauth.py, test_rbac_oauth.py, test_activity_streams.py, models/test_activity_stream.py,
# commands/test_oauth2_token_create.py, commands/test_oauth2_token_revoke.py
main + tests only:  11 failed, 87 passed
with this change:   98 passed

# make test's TEST_DIRS (-n 4 --dist=loadfile), with this change
2 failed, 4631 passed, 6 skipped
# both also fail on main in a plain container: test_logging_aggregator_connection_test_valid
# (no rsyslog config under /var/lib/ascender/rsyslog), test_a_spool_directory_that_cannot_be_entered_falls_back (run as root)

check_migrations: No changes detected
check_settings: Every registered setting has a default its field accepts.
ruff 0.16.6 format --check / check over ascender/: clean
ty 0.0.74 (advisory) on the touched files: 94 diagnostics on main, 93 with this change
OpenAPI schema (ascender-manage spectacular): identical with and without the change, so api.generated.ts doesn't drift

@cigamit cigamit self-assigned this Oct 14, 2026
@cigamit
cigamit requested a balanced review from Copilot October 14, 2026 04:57
POST /api/v2/users/<id>/personal_tokens/ always created a token for
the requester, whatever <id> was, and returned 201. GET on the same
URL listed the requester's tokens. A user created without a password
can't log in to create its own token, so the only way to give one a
token was awx-manage create_oauth2_token on the controller.

The token now belongs to <id>. Users can still create tokens for
themselves; only a superuser may create one for someone else, and
anyone else gets 403. This matches OAuth2TokenAccess, which already
limits reading and revoking personal tokens to the owner and
superusers. Because the sub-list permission check asks can_add about
the user in the URL, non-superusers also get 403 from another user's
tokens/ and authorized_tokens/; superusers there still get their own
token, as before.

A token for someone else is refused (400) when the owner is inactive,
or is a local user while DISABLE_LOCAL_AUTH is on, since turning that
setting on revokes those users' tokens. The external-account check
(ALLOW_OAUTH2_FOR_EXTERNAL_USERS) applies to the owner.

GET lists <id>'s personal tokens, filtered by what the requester may
read. The activity-stream entry for a token whose owner isn't the
actor records the owner in changes.user and links the entry to the
owner's activity stream; the actor is still the requester.

related ctrliq#1116

Assisted-by: Claude Opus 5.5
Signed-off-by: Quanah Gibson-Mount <quanah@dropbox.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The implementation consistently enforces the intended ownership and RBAC behavior with comprehensive functional coverage.

0 open findings

What changed in this PR

Enables superusers to issue personal OAuth2 tokens for another user while preserving RBAC, authentication restrictions, and auditability.

Changes:

  • Uses the URL user as the personal token owner with appropriate validation and permissions.
  • Lists the URL user’s readable personal tokens.
  • Records both issuer and owner in activity streams and updates documentation and tests.
File Description
ascender/​api/​serializers/​oauth2.py Supports serializer-specific token ownership.
ascender/​api/​serializers/​user.py Assigns and validates the target token owner.
ascender/​api/​views/​user.py Lists the target user’s accessible personal tokens.
ascender/​main/​access.py Restricts cross-user issuance to superusers.
ascender/​main/​signals.py Audits and links cross-user token ownership.
ascender/​main/​tests/​functional/​api/​test_oauth.py Covers permissions, validation, listing, authentication, and auditing.
docs/​auth/​oauth.md Updates the endpoint example and ownership guidance.
docs/​docsite/​rst/​administration/​oauth2_token_auth.rst Documents issuance and listing permissions.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@cigamit cigamit added the enhancement New feature or request label Oct 14, 2026
@cigamit

cigamit commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator

Tested and worked as planned. No issues found.

@cigamit
cigamit merged commit 5a0b938 into ctrliq:main Oct 14, 2026
13 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Development

Successfully merging this pull request may close these issues.

3 participants