From feccf1c281c84aa9630367710afbc3945faec86b Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Thu, 8 Oct 2026 21:25:47 +0200 Subject: [PATCH] fix(users): shorten the platform administrator to a name Discourse accepts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 526cb7e212 dropped the hyphen so Keycloak and Mastodon would take the name, and left it one character too long for the third consumer. The Discourse job of run 37799529673 fails while applying the credential: TASK [web-app-discourse : 🔑 Ensure the administrator account carries the current password] Validation failed: Username must be no more than 20 characters (ActiveRecord::RecordInvalid) platform-administrator is 22 characters and platformadministrator is 21, so removing the separator cleared the character rule without clearing the length one, and the second consumer only reported once the first was satisfied. The name is now platformadmin, 13 characters, checked against all four constraints at once: the Keycloak pattern, the Discourse ceiling, the 304 reserved usernames and the names already taken. The role does not configure max_username_length, so Discourse's default of 20 governs. Raising that setting would also have made the deploy pass, but it widens an application's identity policy to fit an account name we choose, which is the wrong direction. The lint gains the ceiling behind the character rule it already enforced, so the next compound name is rejected locally rather than by a Rails validation mid-deploy. Co-Authored-By: Claude Opus 5 (1M context) --- roles/user-administrator/meta/users.yml | 2 +- .../meta/test_identity_username_charset.py | 32 ++++++++++++++----- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/roles/user-administrator/meta/users.yml b/roles/user-administrator/meta/users.yml index 5c04da06f2..68d9afce49 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: platformadministrator + username: platformadmin authorized_keys: [] accounts: [mailbox, identity] diff --git a/tests/lint/ansible/roles/meta/test_identity_username_charset.py b/tests/lint/ansible/roles/meta/test_identity_username_charset.py index 968acb2cd1..98ea25d70e 100644 --- a/tests/lint/ansible/roles/meta/test_identity_username_charset.py +++ b/tests/lint/ansible/roles/meta/test_identity_username_charset.py @@ -8,6 +8,7 @@ from utils.cache.users import get_user_defaults from . import PROJECT_ROOT IDENTITY_USERNAME = re.compile(r"^[a-z0-9]+$") +IDENTITY_USERNAME_MAX = 20 class TestIdentityUsernameCharset(unittest.TestCase): @@ -29,20 +30,35 @@ class TestIdentityUsernameCharset(unittest.TestCase): 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. + + The length ceiling is Discourse's, which answers a longer name with + ``Validation failed: Username must be no more than 20 characters + (ActiveRecord::RecordInvalid)``. It sits behind the character rule: + dropping a hyphen shortens a name by one, so a compound name that + was too long stays too long, and the second consumer only speaks up + once the first is satisfied. """ 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", ""))) - ) + offenders: list[str] = [] + for key, entry in sorted(users.items()): + if "identity" not in (entry.get("accounts") or []): + continue + username = str(entry.get("username", "")) + if not IDENTITY_USERNAME.match(username): + offenders.append( + f"{key}: {username!r} is not lowercase alphanumeric" + ) + elif len(username) > IDENTITY_USERNAME_MAX: + offenders.append( + f"{key}: {username!r} is {len(username)} characters, " + f"over the {IDENTITY_USERNAME_MAX} Discourse allows" + ) 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) + "username one of Keycloak, Mastodon or Discourse refuses:\n" + " - " + "\n - ".join(offenders) )