Repository navigation
Let an administrator create a personal access token for another user #1116
Description
Please confirm the following
- I have checked the current issues for duplicates.
- I understand that Ascender is open source software provided for free and that I might not receive a timely response.
Feature type
Enhancement to Existing Feature
Feature Summary
Assisted-by: Claude Opus 5.5. Drafted with AI assistance; every code path cited was read at main c5da6e18, and every result below was reproduced with functional tests against that commit.
Let a system administrator create a personal access token for another user through POST /api/v2/users/<id>/personal_tokens/, typically for a service account created without a password (#1089 / #1090), which can't log in to create its own.
Today the API has no way to do this. Every token serializer sets the owner to the requesting user (BaseOAuth2TokenSerializer.create), so POST /api/v2/users/<id>/personal_tokens/ ignores <id>: as an admin it returns 201 with a token that belongs to the admin, which reads like success. GET on the same URL lists the requester's tokens whatever <id> is. The only way to give another user a token is awx-manage create_oauth2_token --user <name> on the controller, which declarative tooling that drives the API can't reach.
Select the relevant components
- UI
- API
- Docs
- Collection
- CLI
- Other
Steps to reproduce
- On a new installation, as
admin, create a user:POST /api/v2/users/with{"username": "svc-ci", "password": "<any>"}(or without a password once Allow creating a local user without a password #1090 is in). Note itsid. - As
admin:POST /api/v2/users/<svc-ci id>/personal_tokens/with{"description": "CI", "scope": "write"}. - Look at the
userfield of the response, andGET /api/v2/me/withAuthorization: Bearer <token>. - As
admin:GET /api/v2/users/<svc-ci id>/personal_tokens/. - As
admin:POST /api/v2/users/9999/personal_tokens/(no such user). - Add
svc-cito theDefaultorganization, create a second userorgadminand make it that organization's admin. Asorgadmin:POST /api/v2/users/<svc-ci id>/personal_tokens/.
Current results
- Step 2 returns
201. The response'suseris the admin's id, and in step 3 the token authenticates asadmin, notsvc-ci. - Step 4 lists the admin's own personal tokens, not
svc-ci's. - Step 5 returns
201and creates another token foradmin. - Step 6 returns
201and creates a token fororgadmin. The same goes for a system auditor, and for any user who can see the target. The documentation's example (docs/auth/oauth.md) uses the literal URL/api/v2/users/1/personal_tokens/, and every user can see user 1 (system administrators are always visible), so whoever follows it gets their own token regardless. - A requester who can't see the target user gets
403for bothPOSTandGET, so the misleading201happens exactly when the requester can see the user named in the URL. - The activity-stream entry for the token names only the actor;
changeshas no owner, becauseuseris read-only in the token serializer.
Sugested feature result
POST /api/v2/users/<id>/personal_tokens/ creates a token owned by <id>:
| Requester | Today | Proposed |
|---|---|---|
<id> themselves |
201, own token |
unchanged |
| superuser, another user | 201, superuser's own token |
201, token owned by <id> |
| superuser, inactive user | 201, superuser's own token |
400 "Cannot create a token for an inactive user." |
superuser, local user while DISABLE_LOCAL_AUTH is on |
201, superuser's own token |
400 "Cannot create a token for a local user while local authentication is disabled." |
| superuser, no such user | 201, superuser's own token |
404 |
anyone else (org admin, system auditor, normal user) who can see <id> |
201, own token |
403 |
anyone who can't see <id> |
403 |
403 |
- The external-account check (
ALLOW_OAUTH2_FOR_EXTERNAL_USERS) applies to the owner, so a superuser can't create a token for an LDAP/SAML/RADIUS user when that setting forbids those users from having one (403, the existing message). - "Local" in the
DISABLE_LOCAL_AUTHcase is the same testDisableLocalAuthMiddlewareuses (no LDAP DN, social or enterprise auth). Turning that setting on revokes those users' tokens, so issuing new ones afterward would undo it. GET /api/v2/users/<id>/personal_tokens/lists<id>'s personal tokens, limited to what the requester may read underOAuth2TokenAccess: superusers and system auditors see them,<id>sees their own, anyone else who can see<id>gets an empty list.- The activity-stream entry for a token whose owner isn't the actor names both:
actoris the administrator,changes.useris the owner ("svc-ci (7)"), and the entry also appears in the owner's/api/v2/users/<id>/activity_stream/.
Why superusers only, rather than anyone who can change the user: UserAccess.can_change/can_admin let an organization admin edit a member, but OAuth2TokenAccess limits reading, changing and revoking a personal token to its owner and superusers. If org admins could issue a token for a member, they couldn't list or revoke it afterward. Extending this to org admins would mean widening personal-token read and delete for them at the same time, which is a bigger change. The access check is a single condition in OAuth2TokenAccess.can_add, so it's easy to widen later if you want that.
Additional information
Prior art upstream: ansible/awx#1653 (2018, "Allow Token Creation for other Users in the REST API", still open) asks for the same thing and mentions create_oauth2_token as the workaround. ansible/awx#15159 (2024, open) reports the GET half. AWX then removed its OAuth2 provider (ansible/awx#15666), so neither will be fixed there. django-ansible-base's replacement makes the user's personal_tokens sub-URL read-only and filters it by the user in the URL, without adding issuance.
Compatibility: this changes what an existing endpoint returns.
- A superuser who posts to another user's URL now gets a token owned by that user instead of themselves. A script that hard-codes another id (for example the documentation's
/api/v2/users/1/) and runs as a different superuser would now get a token for user 1. The response'suserfield shows the owner, and the attached PR updates the documentation's example to<user_id>. - A non-superuser posting to someone else's URL gets
403instead of201and their own token. A superuser posting to a nonexistent id gets404. - The same
403applies to a non-superuser'sPOSTto someone else's/api/v2/users/<id>/tokens/and/authorized_tokens/. Those views share the permission check, which asksOAuth2TokenAccess.can_addabout the user in the URL. Superusers posting there still get a token of their own, as today. GETon another user's URL returns that user's tokens (superusers, auditors) or an empty list, instead of the requester's own.- Unchanged:
/api/v2/tokens/(which the UI andctrliq.ascenderuse for personal tokens) and the user's own/tokens/and/authorized_tokens/still create tokens for the requester. The in-repo UI doesn't callpersonal_tokens/and shows the Tokens tab only on your own user page, so it is unaffected. The OpenAPI schema doesn't change. - Activity stream: token-create entries whose owner isn't the actor gain
changes.userand the owner link. That includes tokens created with no request user, such as bycreate_oauth2_token, whose entries name nobody today. Entries for tokens users create for themselves are unchanged.
Not covered: a superuser's POST to another user's /api/v2/users/<id>/tokens/ or /authorized_tokens/ still creates the superuser's own token, and GET .../authorized_tokens/ lists the requester's tokens. The same approach would work for both; I left them out to keep this to the personal-token case. Token deletion entries still don't name the owner.
I have a PR ready that implements this, with tests.
Activity
Let a superuser issue a personal token for another user