feat(tokens): Master-Tokens nachtraeglich bearbeiten

Bisher erzwang jede Korrektur ein neues Token: ein Schreibfehler im Namen, ein
falsch gewaehlter Identitaetstyp, ein nachtraeglich gebrauchtes Recht. Das ist
nicht nur laestig, sondern gefaehrlich - der neue Wert muss auf alle Maschinen
verteilt werden, und am Ende laeuft das alte weiter, weil eine vergessen wurde.

AENDERBAR ist alles ausser dem Tokenwert selbst, der Nummer, dem Typ und dem
Anlagedatum: Name, Identitaetstyp, freie Identitaet, Projekt, Lizenzschluessel,
Umgebung, Ablaufdatum und die Rechte.

Der Wert bleibt unberuehrt, weil er es muss - er liegt nur als Hash vor und
steht auf fremden Maschinen. Ein "geaenderter" Wert waere ein neues Token mit
alter Nummer. Genau diese Zusage haelt ein eigener Testfall fest: derselbe rohe
Token muss nach der Aenderung weiter funktionieren.

  - TokenManager::updateMasterToken() mit zwei Schranken: leerer Name und
    leere Rechteliste werden abgewiesen. Ein Token ohne Rechte sieht gueltig
    aus und laesst jeden Aufruf mit "unauthorized" scheitern, ohne dass der
    Grund sichtbar waere.
  - Was sich geaendert hat, geht ins Log. Eine stille Rechteaenderung an einem
    Token ist genau das, was man spaeter rekonstruieren will.
  - Dialog in der Token-Verwaltung, erreichbar ueber den Stift in der Zeile.
    Zeigt Nummer und Hinweis, dass der Wert unveraendert bleibt.
  - Haengen Sub-Tokens daran, sagt der Dialog, dass eine Einschraenkung NICHT
    rueckwirkend auf sie durchschlaegt - sie haben ihre Rechte beim Ausstellen
    bekommen. Wer die Kinder beschneiden will, widerruft und stellt neu aus.

$scopeChoices ist aus dem Anlegeformular in den Datenteil gewandert. Beide
Formulare lesen jetzt dieselbe Liste - eine an zwei Stellen gefuehrte Liste
haette genau den Fehler wiederholt, der eben erst behoben wurde: Die
Supervisor-Rechte fehlten monatelang im Formular, obwohl TokenManager sie kennt.

Testlauf 136 Faelle. Da die Bearbeitung bewusst keine API hat - ein Token soll
seine eigenen Rechte nicht erweitern koennen - fuehrt der Weg dorthin ueber
testctl.php?action=update_master_token, wie schon beim Werkbank-Token.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Deploymentcenter Bot
2026-08-30 14:04:02 +02:00
co-authored by Claude Opus 5
parent eeda6c911e
commit 57aa0bc5be
5 changed files with 455 additions and 22 deletions
+84
View File
@@ -246,6 +246,90 @@ def _(ctx):
erwarte_fehler(r, http=(401, 403))
# ---------------------------------------------------------------------------
# Token nachtraeglich aendern
#
# Bis 2.7.0 ging das nicht: Ein Schreibfehler im Namen oder ein nachtraeglich
# gebrauchtes Recht erzwang ein komplett neues Token - mit der Folge, dass der
# neue Wert auf alle Maschinen verteilt werden muss und am Ende das alte
# mitlaeuft, weil eine vergessen wurde.
# ---------------------------------------------------------------------------
@test('token', 'Aenderbares Token laesst sich anlegen')
def _(ctx):
r = ctx.testctl('workbench_token', project_slug='myapp', name='vorher')
erwarte_erfolg(r)
ctx['edit_token_id'] = r.feld('token_id')
ctx['edit_token_raw'] = r.feld('raw_token')
@test('token', 'Name und Rechte lassen sich nachtraeglich aendern')
def _(ctx):
r = ctx.testctl('update_master_token',
token_id=ctx['edit_token_id'],
name='nachher',
owner_type='dev_agent',
project_slug='myapp',
environment='development',
scopes=['bugtracker:read', 'supervisor:read'])
erwarte_erfolg(r)
erwarte(r.feld('token', 'name') == 'nachher',
'Name nicht uebernommen: %r' % r.feld('token', 'name'))
erwarte(r.feld('token', 'environment') == 'development',
'Umgebung nicht uebernommen: %r' % r.feld('token', 'environment'))
erwarte('supervisor:read' in (r.feld('token', 'scopes') or ''),
'Rechte nicht uebernommen: %r' % r.feld('token', 'scopes'))
@test('token', 'Der Tokenwert bleibt bei einer Aenderung derselbe')
def _(ctx):
"""
Die eigentliche Zusage der Funktion. Waere der Wert danach ein anderer,
muessten alle Maschinen neu bestueckt werden - dann koennte man auch
gleich ein neues Token ausstellen, und die Bearbeitung waere sinnlos.
"""
r = ctx.client.get('/api/bugtracker/v1/projects', token=ctx['edit_token_raw'])
erwarte_erfolg(r)
@test('token', 'Entzogene Rechte greifen sofort')
def _(ctx):
"""
Die Gegenprobe: Der Wert bleibt, die Rechte nicht. Das Token hatte
bugtracker:manage und hat es nach der Aenderung nicht mehr.
"""
r = ctx.client.post('/api/bugtracker/v1/manage',
query={'action': 'next'},
token=ctx['edit_token_raw'],
body={'limit': 1})
erwarte_fehler(r, http=(401, 403))
@test('token', 'Aenderung ohne Rechte wird abgewiesen')
def _(ctx):
"""
Ein Token ohne jedes Recht sieht gueltig aus und kann nichts - der
Aufrufer bekaeme "unauthorized" ohne erkennbaren Grund.
"""
r = ctx.testctl('update_master_token', token_id=ctx['edit_token_id'],
name='ohne rechte', scopes=[])
erwarte_fehler(r, http=400, code='invalid_update')
@test('token', 'Aenderung ohne Namen wird abgewiesen')
def _(ctx):
r = ctx.testctl('update_master_token', token_id=ctx['edit_token_id'],
name=' ', scopes=['bugtracker:read'])
erwarte_fehler(r, http=400, code='invalid_update')
@test('token', 'Aenderung an einem fehlenden Token antwortet 404')
def _(ctx):
r = ctx.testctl('update_master_token', token_id='tok_m_gibtsnicht',
name='egal', scopes=['bugtracker:read'])
erwarte_fehler(r, http=404, code='not_found')
# ---------------------------------------------------------------------------
# Allgemeines Verhalten der Schnittstelle
# ---------------------------------------------------------------------------