feat(authentik): reject colliding usernames case-insensitively at registration

Authentik's own uniqueness is case-sensitive, so 'Boje' and 'boje' pass as
distinct while Matrix treats them as the same localpart. ADR-0011 closed the
takeover vector with on_conflict:fail, but that only bites at login: the user
registers happily and fails later with no explanation. This policy answers where
the mistake is made.

Deliberately reads only prompt_data and never request.user — the stage runs in an
anonymous enrollment context, which is exactly what the previously attached system
policies crashed on.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Thore Cimbal
2026-08-15 12:00:00 +00:00
co-authored by Claude Opus 4.8
parent b4650dcef8
commit afc4ad3f20
+34 -1
View File
@@ -28,12 +28,45 @@ data:
# from manual UI setup, likely a "select all" slip in the policy picker.
# These crash on an anonymous enrollment context ('AnonymousUser' object
# has no attribute 'group_attributes', etc). A prompt stage needs none here.
# Eindeutigkeit case-insensitiv pruefen (#0043). Authentiks eigene Pruefung
# ist case-sensitiv, "Boje" und "boje" gelten ihr also als verschieden --
# in Matrix kollidieren sie. Seit ADR-0011 faengt MAS das mit
# on_conflict:fail ab, aber erst BEIM LOGIN: der Nutzer registriert sich
# erfolgreich und scheitert spaeter ohne Erklaerung. Diese Policy gibt die
# Rueckmeldung dort, wo der Fehler entsteht.
#
# ⚠️ Bewusst ohne Zugriff auf request.user: die Stage laeuft im ANONYMEN
# Enrollment-Kontext. Genau daran sind die frueher hier haengenden
# System-Policies gescheitert ('AnonymousUser' hat kein group_attributes).
# Gelesen wird ausschliesslich prompt_data.
- model: authentik_policies_expression.expressionpolicy
state: present
identifiers:
name: matrix-username-eindeutig-ci
id: username_unique_ci
attrs:
execution_logging: false
expression: |
from authentik.core.models import User
gewuenscht = (request.context.get("prompt_data") or {}).get("username") or ""
gewuenscht = gewuenscht.strip()
if not gewuenscht:
return True # Pflichtfeld-Pruefung macht der Prompt selbst
if User.objects.filter(username__iexact=gewuenscht).exists():
ak_message("Dieser Benutzername ist bereits vergeben - auch in anderer "
"Gross-/Kleinschreibung. Bitte waehle einen anderen.")
return False
return True
- model: authentik_stages_prompt.promptstage
state: present
identifiers:
name: matrix-invitation-prompt
attrs:
validation_policies: []
validation_policies:
- !KeyOf username_unique_ci
# Correct stage chain, mirroring the working matrix-enrollment flow:
# Invite -> Prompt (username/email/password) -> Write -> Password -> Login