Repository navigation
Allow creating a local user without a password - #1090
Conversation
df86704 to
602ccdc
Compare
There was a problem hiding this comment.
🟢 Approval recommended
The focused implementation matches the requested behavior and includes appropriate regression coverage.
0 open findings
What changed in this PR
Enables token-only local accounts by allowing user creation without a password and assigning an unusable password.
Changes:
- Bypasses password validation when a new user omits or blanks the password.
- Adds functional coverage for omitted, blank, and complexity-policy scenarios.
| File | Description |
|---|---|
ascender/api/serializers/user.py |
Allows passwordless creation while preserving validation for supplied passwords. |
ascender/main/tests/functional/api/test_user.py |
Verifies passwordless users receive unusable passwords. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Thanks for this. The change does what it says: an API-created user with no password, or 1. A RADIUS login can take over password-less local users
if category == 'radius':
ret = ret or not user.has_usable_password()Before this PR, the API couldn't create a local user in that state. Now it can, so when RADIUS is configured,
So a RADIUS user named Suggested fix: the comment there already says to remove the fallback "in a future release". Either drop it, or require a (This might not be a big deal though, as we have another PR that drops RADIUS auth) 2. The API has no way to give these accounts a tokenThe PR targets accounts that only authenticate with an OAuth2 token, but nothing in the API can create a token for a user who can't log in:
The only working route I found is |
|
Thanks for testing this so thoroughly. Pushed 4a06149 (Stop treating every password-less user as a RADIUS user): 1. RADIUS takeover: fixed by dropping the fallback. With it gone, RADIUS needs a
2. Tokens: confirmed. Creating a personal token as admin for another user gives 201 with the token owned by the admin, and Full |
validate_password rejected a new user with no password, although _update_password already stores an unusable password in that case. A token-only service account then needs a password nobody should use. Return early for a new user with no password; supplied passwords keep every existing check, and updates are unchanged. related ctrliq#1089 Assisted-by: Claude Opus 5.5 Signed-off-by: Quanah Gibson-Mount <quanah@dropbox.com>
user_is_in_enterprise_category() accepted any user without a usable password as a RADIUS user, a fallback for accounts created before UserEnterpriseAuth existed. With password-less local users now creatable through the API, a RADIUS principal with the same username could log in as such an account, superuser included. Drop the fallback, so RADIUS needs a provider='radius' UserEnterpriseAuth row, as TACACS+ already does. Every RADIUS user created since that model was introduced (2017, before AWX 1.0.0) has one, so existing RADIUS users keep logging in. Tests: a password-less local user is not adopted by RADIUS, through the helper and end to end through RADIUSBackend; a user with a RADIUS row still is. related ctrliq#1089 Assisted-by: Claude Opus 5.5 Signed-off-by: Quanah Gibson-Mount <quanah@dropbox.com>
On ansible/awx#16688: it doesn't need the RADIUS change. AWX itself no longer authenticates RADIUS users: ansible/awx#15548 removed it, |
cigamit
left a comment
There was a problem hiding this comment.
LGTM, tested everything again and its now all working as expected.
SUMMARY
Assisted-by: Claude Opus 5.5. Drafted with AI assistance; the commits carry the same trailer.
Lets
POST /api/v2/users/create a local user without a password, giving it an unusable one, for accounts that only ever authenticate with an OAuth2 token.UserSerializer._update_passwordalready callsset_unusable_password()when no password is given, so the only change is thatvalidate_passwordreturns early for a new user with no password, before Django'svalidate_password()and instead of raisingPassword required for new User.Passwords that are supplied keep every existing check. Updating an existing user is unchanged.A second commit stops RADIUS from claiming these accounts.
user_is_in_enterprise_category()treated any user without a usable password as a RADIUS user, a fallback for users created beforeUserEnterpriseAuthexisted, so a RADIUS principal with the same username could have logged in as a password-less local account. RADIUS now needs aprovider='radius'UserEnterpriseAuthrow, as TACACS+ already does. Every RADIUS login since that model was added (2017, before AWX 1.0.0) created one, so existing RADIUS users are unaffected. This is the same change the RADIUS removal in #1068 makes to that function.Other readers of the password state were checked and don't adopt these accounts: LDAP requires
ldap_dn, TACACS+ requires its row, SAML and the other social backends match on their own association (or an email match, independent of the password), andget_external_account()reports them as local.Same change as ansible/awx#16688 (for ansible/awx#16687), adjusted for Ascender's extra Django-validator call.
related #1089
ISSUE TYPE
COMPONENT NAME
ASCENDER VERSION
ADDITIONAL INFORMATION
Getting a token for a password-less account. The API can't issue one: token creation always uses the requesting user. As an admin,
POST /api/v2/users/<id>/personal_tokens/returns201, but the token belongs to the admin, not<id>. Create the token on the controller instead:It prints a personal access token (no application,
writescope, expiry fromOAUTH2_PROVIDER['ACCESS_TOKEN_EXPIRE_SECONDS']) that authenticates assvc-ci.ascender-manageis the same command. These accounts count as local, so turning onDISABLE_LOCAL_AUTHrevokes their tokens as it does for other local users. Letting an admin issue a token for another user through the API is a separate change, not part of this PR.Tests.
test_user_create_without_password(password omitted and""),test_user_create_without_password_ignores_complexity_settingsandtest_user_created_without_password_is_not_a_radius_userintest_user.py(the last one end to end throughRADIUSBackendwith a stubbed RADIUS reply);test_radius_does_not_adopt_local_user_without_passwordandtest_radius_user_with_enterprise_auth_logs_inintest_get_or_set_enterprise_user.py.