From 526cb7e212e6b6eeebeb9fa1706ce3e827c558aa Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Thu, 8 Oct 2026 18:47:02 +0200 Subject: [PATCH] fix(users): give the new admin accounts usernames Keycloak accepts Run 37799529673 fails three jobs on the two accounts 3c72c2002a introduced, because both were named with a hyphen: failed: [localhost] (item=keycloak-bootstrap) failed: [localhost] (item=platform-administrator) stderr: Username is reserved or contains invalid characters. Only lowercase letters (a-z) and digits (0-9) are allowed. That message is this role's own policy: the Keycloak user profile pins `^(?!reserved)[a-z0-9]+$` in templates/import/components/org.keycloak.userprofile.UserProfileProvider.json.j2. Mastodon refuses the same name independently, with `bin/tootctl accounts create ... Failure/Error: account.username invalid`, so loosening the Keycloak pattern would have fixed neither job. The usernames become platformadministrator and keycloakbootstrap. Every other hyphenated user in the repository is exempt by having no `identity` account, so Keycloak never sees it; these two were the only identity-registered names that violated the pattern, which is why nothing caught them before a realm import ran. The lint closes that gap: it resolves the real user set and fails when an identity user's username is not lowercase alphanumeric, so the next such name is rejected locally instead of fourteen hours later in a deploy job. Co-Authored-By: Claude Opus 5 (1M context) --- roles/user-administrator/meta/users.yml | 2 +- roles/web-app-keycloak/meta/users.yml | 2 +- .../meta/test_identity_username_charset.py | 50 +++++++++++++++++++ 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 tests/lint/ansible/roles/meta/test_identity_username_charset.py diff --git a/roles/user-administrator/meta/users.yml b/roles/user-administrator/meta/users.yml index 5a280cd3cb..5c04da06f2 100644 --- a/roles/user-administrator/meta/users.yml +++ b/roles/user-administrator/meta/users.yml @@ -3,7 +3,7 @@ platform_administrator: description: Application administrator for {{ ORGANIZATION }} on {{ DOMAIN_PRIMARY }} firstname: Platform lastname: Administrator - username: platform-administrator + username: platformadministrator authorized_keys: [] accounts: [mailbox, identity] diff --git a/roles/web-app-keycloak/meta/users.yml b/roles/web-app-keycloak/meta/users.yml index d8aa494e97..abf2e8c78f 100644 --- a/roles/web-app-keycloak/meta/users.yml +++ b/roles/web-app-keycloak/meta/users.yml @@ -1,6 +1,6 @@ --- keycloak_bootstrap: - username: keycloak-bootstrap + username: keycloakbootstrap roles: - bot platform_administrator: {} diff --git a/tests/lint/ansible/roles/meta/test_identity_username_charset.py b/tests/lint/ansible/roles/meta/test_identity_username_charset.py new file mode 100644 index 0000000000..968acb2cd1 --- /dev/null +++ b/tests/lint/ansible/roles/meta/test_identity_username_charset.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +import re +import unittest + +from utils.cache.users import get_user_defaults + +from . import PROJECT_ROOT + +IDENTITY_USERNAME = re.compile(r"^[a-z0-9]+$") + + +class TestIdentityUsernameCharset(unittest.TestCase): + def test_every_identity_username_is_lowercase_alphanumeric(self): + """ + A user whose ``accounts`` contain ``identity`` MUST have a username of + lowercase letters and digits only. + + Two consumers reject anything else, and both reject it at deploy time + rather than at render time. Keycloak's own user profile pins the + pattern ``^(?!reserved)[a-z0-9]+$`` + (``roles/web-app-keycloak/templates/import/components/ + org.keycloak.userprofile.UserProfileProvider.json.j2``) and answers a + violation with "Username is reserved or contains invalid characters". + Mastodon's ``bin/tootctl accounts create`` answers with + ``Failure/Error: account.username invalid``. + + A hyphen is the way this breaks, because it is the natural separator + for a compound account name and every other hyphenated user in the + repository is exempt by having no ``identity`` account, so nothing + catches it until a realm import or a tootctl call runs. + """ + users = get_user_defaults(roles_dir=str(PROJECT_ROOT / "roles")) + offenders = sorted( + f"{key}: {entry['username']}" + for key, entry in users.items() + if "identity" in (entry.get("accounts") or []) + and not IDENTITY_USERNAME.match(str(entry.get("username", ""))) + ) + + if offenders: + self.fail( + "These users are registered as an identity but carry a " + "username Keycloak and Mastodon both refuse (lowercase " + "letters and digits only):\n - " + "\n - ".join(offenders) + ) + + +if __name__ == "__main__": + unittest.main()