feat: Ein Benutzername laesst sich aendern – er ist ein Etikett, kein Schluessel
Aendert der Anmeldedienst den Namen eines Nutzers, muss Certain folgen
koennen. Bisher ging das gar nicht: Umbenennen war nirgends vorgesehen.
Die gute Nachricht vorweg – am Fundament war nichts zu richten. Buchungen,
Konten, Vertraege, Sitzungen und Geraeteschluessel haengen alle an der NUMMER
des Nutzers; der Name ist nirgends ein Fremdschluessel, und auch das
Wiedereinspielen einer Sicherung ordnet nicht ueber ihn zu. Umbenennen zieht
also nichts mit. Ein Test haelt das jetzt fest: Nach dem Umbenennen sind es
dieselbe Nummer, dasselbe Konto und derselbe Saldo.
Zwei Wege gibt es jetzt.
Der Admin kann umbenennen (PATCH /api/users/{id}). Eindeutig bleiben muss der
Name trotzdem – er ist das, was beim Passwort-Login eingetippt wird.
Und der Anmeldedienst zieht bei jeder Anmeldung nach. Dabei zaehlt, WESSEN
Name das jeweils ist: Der Anzeigename gehoert dem Anbieter – heiratet jemand,
soll der neue Name auch hier stehen. Der Benutzername gehoert bei einem Konto
MIT Passwort dem Nutzer; ihn hinter dessen Ruecken zu aendern hiesse, ihm
seine Anmeldung wegzunehmen. Nur bei einem reinen SSO-Konto gibt es nichts
einzutippen – dort darf der Anbieter bestimmen.
Ist der neue Name schon vergeben, bleibt der alte stehen. Eine Anmeldung
daran scheitern zu lassen waere die schlechtere Antwort.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
62cb6a017c
commit
41db67f53e
@@ -178,6 +178,44 @@ def _darf_anlegen(p, anmeldung: oidc_service.Anmeldung) -> bool:
|
||||
return anmeldung.email.rsplit("@", 1)[-1] in p.allowed_domains
|
||||
|
||||
|
||||
def _uebernimm_namen(db: Session, nutzer: User, anmeldung: oidc_service.Anmeldung) -> None:
|
||||
"""Namensaenderungen beim Anbieter nachziehen.
|
||||
|
||||
Wichtig ist, WESSEN Name das jeweils ist:
|
||||
|
||||
``display_name`` ist reine Anzeige und gehoert dem Anbieter – heiratet
|
||||
jemand, soll der neue Name auch hier stehen.
|
||||
|
||||
``username`` ist das, was beim Passwort-Login eingetippt wird. Bei einem
|
||||
Konto MIT Passwort gehoert er dem Nutzer; ihn hinter dessen Ruecken zu
|
||||
aendern hiesse, ihm seine Anmeldung wegzunehmen. Bei einem reinen
|
||||
SSO-Konto gibt es nichts einzutippen – dort ist er ein Etikett, und der
|
||||
Anbieter darf es bestimmen.
|
||||
|
||||
Dass das ueberhaupt geht, liegt daran, dass der Name NIRGENDS ein
|
||||
Schluessel ist: Buchungen, Konten und Sitzungen haengen an der Nummer des
|
||||
Nutzers. Umbenennen zieht nichts mit.
|
||||
|
||||
Ist der neue Name schon vergeben, bleibt der alte stehen. Eine Anmeldung
|
||||
daran scheitern zu lassen waere die schlechtere Antwort.
|
||||
"""
|
||||
if anmeldung.name and anmeldung.name[:120] != nutzer.display_name:
|
||||
nutzer.display_name = anmeldung.name[:120]
|
||||
|
||||
gewuenscht = (anmeldung.preferred_username or "").strip()[:64]
|
||||
if not gewuenscht or gewuenscht == nutzer.username:
|
||||
return
|
||||
if nutzer.password_hash is not None:
|
||||
return # gehoert dem Nutzer, nicht dem Anbieter
|
||||
belegt = (
|
||||
db.query(User.id)
|
||||
.filter(User.username == gewuenscht, User.id != nutzer.id)
|
||||
.first()
|
||||
)
|
||||
if belegt is None:
|
||||
nutzer.username = gewuenscht
|
||||
|
||||
|
||||
def _finde_oder_lege_an(db: Session, p, anmeldung: oidc_service.Anmeldung) -> User:
|
||||
"""Zu wem gehoert dieser Anmeldende?
|
||||
|
||||
@@ -205,6 +243,7 @@ def _finde_oder_lege_an(db: Session, p, anmeldung: oidc_service.Anmeldung) -> Us
|
||||
nutzer = db.get(User, vorhanden.user_id)
|
||||
if nutzer is None:
|
||||
raise oidc_service.OidcFehler("Das verknuepfte Konto gibt es nicht mehr.")
|
||||
_uebernimm_namen(db, nutzer, anmeldung)
|
||||
return nutzer
|
||||
|
||||
if not _darf_anlegen(p, anmeldung):
|
||||
|
||||
@@ -49,6 +49,23 @@ def update_user(user_id: int, payload: UserUpdate, db: Session = Depends(get_db)
|
||||
user = db.get(User, user_id)
|
||||
if user is None:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Benutzer nicht gefunden")
|
||||
if payload.username is not None:
|
||||
neu = payload.username.strip()
|
||||
if not neu:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST,
|
||||
"Benutzername darf nicht leer sein")
|
||||
# Der Name ist ein Etikett, kein Schluessel – umbenennen zieht nichts
|
||||
# mit. Eindeutig bleiben muss er trotzdem: Er ist das, was beim
|
||||
# Passwort-Login eingetippt wird.
|
||||
belegt = (
|
||||
db.query(User.id)
|
||||
.filter(User.username == neu, User.id != user.id)
|
||||
.first()
|
||||
)
|
||||
if belegt is not None:
|
||||
raise HTTPException(status.HTTP_409_CONFLICT,
|
||||
"Benutzername existiert bereits")
|
||||
user.username = neu
|
||||
if payload.password is not None:
|
||||
policy_service.assert_ok(db, payload.password)
|
||||
user.password_hash = hash_password(payload.password)
|
||||
|
||||
@@ -57,6 +57,11 @@ class UserCreate(BaseModel):
|
||||
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
# Umbenennen ist erlaubt: Der Benutzername ist ein ETIKETT, kein Schluessel.
|
||||
# Alles Fachliche haengt an der Nummer des Nutzers – aendert sich der Name
|
||||
# beim Anmeldedienst, muss Certain folgen koennen, ohne dass Daten
|
||||
# umziehen.
|
||||
username: str | None = Field(default=None, min_length=1, max_length=64)
|
||||
password: str | None = Field(default=None, min_length=1, max_length=1024)
|
||||
role: Role | None = None
|
||||
display_name: str | None = Field(default=None, max_length=120)
|
||||
|
||||
Reference in New Issue
Block a user