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:
co-authored by
Claude Opus 5
parent
eeda6c911e
commit
57aa0bc5be
+226
-20
@@ -959,6 +959,39 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
dc_redirect('#tab-tokens');
|
||||
}
|
||||
|
||||
case 'edit_master_token': {
|
||||
$tokenId = trim((string)($_POST['token_id'] ?? ''));
|
||||
$scopes = $_POST['scopes'] ?? [];
|
||||
|
||||
try {
|
||||
$ok = (new CoreTokenManager($pdo))->updateMasterToken($tokenId, [
|
||||
'name' => $_POST['name'] ?? '',
|
||||
'project_slug' => $_POST['project_slug'] ?? '',
|
||||
'license_key' => $_POST['license_key'] ?? '',
|
||||
'owner_type' => $_POST['owner_type'] ?? '',
|
||||
'owner_identity' => $_POST['owner_identity'] ?? '',
|
||||
'environment' => $_POST['environment'] ?? '',
|
||||
// Das Datumsfeld liefert nur den Tag; das Token soll
|
||||
// an diesem Tag noch gelten und erst danach ablaufen.
|
||||
'expires_at' => !empty($_POST['expires_at'])
|
||||
? $_POST['expires_at'] . ' 23:59:59' : '',
|
||||
'scopes' => is_array($scopes) ? $scopes : [],
|
||||
]);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
dc_flash($e->getMessage(), 'danger');
|
||||
dc_redirect('#tab-tokens');
|
||||
}
|
||||
|
||||
dc_flash(
|
||||
$ok
|
||||
? 'Token-Angaben gespeichert. Der Tokenwert selbst ist unveraendert — '
|
||||
. 'auf den Maschinen muss nichts ausgetauscht werden.'
|
||||
: 'Master-Token nicht gefunden.',
|
||||
$ok ? 'success' : 'danger'
|
||||
);
|
||||
dc_redirect('#tab-tokens');
|
||||
}
|
||||
|
||||
case 'revoke_core_token': {
|
||||
$tokenId = trim((string)($_POST['token_id'] ?? ''));
|
||||
if ($tokenId !== '') {
|
||||
@@ -2085,6 +2118,33 @@ function rmBadge(string $status): string
|
||||
return '<span class="badge ' . $class . '">' . $marker . ' ' . e($label) . '</span>';
|
||||
}
|
||||
|
||||
// Die auswaehlbaren Rechte - EINE Liste fuer Anlegen und Bearbeiten.
|
||||
//
|
||||
// Vorher stand sie im Anlegeformular. Genau deshalb fehlten die
|
||||
// Supervisor-Rechte monatelang: Wer eine Liste an zwei Stellen fuehrt,
|
||||
// pflegt eine davon nicht. Ein zweites Formular waere die zweite Stelle
|
||||
// gewesen.
|
||||
$scopeChoices = [
|
||||
'*' => ['⚠️ Alle Rechte', 'Uneingeschränkt — auch Releases veröffentlichen und Tokens ausstellen. Nicht an einen Agenten geben.'],
|
||||
'bugtracker:manage' => ['⚙️ Bugtracker verwalten', 'Übernehmen, kommentieren, schließen — schließt Lesen und Melden ein'],
|
||||
'bugtracker:report' => ['🐛 Bugtracker melden', 'Nur Einträge anlegen — schließt Lesen ein'],
|
||||
'bugtracker:read' => ['👁️ Bugtracker lesen', 'Nur Abfragen'],
|
||||
'watchdog:ping' => ['🛡️ Watchdog Heartbeat', 'Statusmeldungen senden'],
|
||||
'watchdog:read' => ['📊 Watchdog lesen', 'Monitore und Ereignisse abfragen'],
|
||||
// Die Supervisor-Rechte fehlten hier, obwohl TokenManager
|
||||
// sie seit 2.6.0 kennt. Folge: Ein Werkbank-Token liess
|
||||
// sich ueber die Oberflaeche gar nicht vollstaendig
|
||||
// ausstellen - dc_findings und dc_metrics im MCP-Adapter
|
||||
// liefen ins unauthorized, und der Grund war nirgends zu
|
||||
// sehen.
|
||||
'supervisor:read' => ['🔎 Befunde lesen', 'Supervisor-Befunde abfragen — braucht ein Werkbank-Token für dc_findings'],
|
||||
'supervisor:manage' => ['🔬 Befunde einstufen', 'Befunde abhaken und bewerten — schließt Lesen ein'],
|
||||
'supervisor:report' => ['📡 Befunde melden', 'Nur einliefern, etwa aus einem Prüfwerkzeug — schließt Lesen ein'],
|
||||
'updateservice:read' => ['📦 Updates prüfen', 'Auf neue Releases prüfen'],
|
||||
'updateservice:publish' => ['🚀 Releases veröffentlichen', 'Neue Releases einstellen — schließt Prüfen ein'],
|
||||
'license:dev' => ['🧪 DEV-Lizenz abholen', 'Befristeten Testschlüssel selbst ausstellen — nur für Werkbank-Token auf Testmaschinen'],
|
||||
];
|
||||
|
||||
// Agenten-Uebersicht: wer bearbeitet gerade was
|
||||
$agentWorkload = $pdo->query('
|
||||
SELECT claimed_by,
|
||||
@@ -5303,6 +5363,19 @@ Antwort:
|
||||
</td>
|
||||
<td>
|
||||
<div style="display:flex; gap:0.3rem;">
|
||||
<button type="button" class="btn btn-sm btn-secondary" title="Angaben ändern"
|
||||
onclick='openTokenEdit(<?= htmlspecialchars(json_encode([
|
||||
"token_id" => (string)$mTok["token_id"],
|
||||
"name" => (string)$mTok["name"],
|
||||
"owner_type" => (string)$mTok["owner_type"],
|
||||
"owner_identity" => (string)($mTok["owner_identity"] ?? ""),
|
||||
"project_slug" => (string)($mTok["project_slug"] ?? ""),
|
||||
"license_key" => (string)($mTok["license_key"] ?? ""),
|
||||
"environment" => (string)$mTok["environment"],
|
||||
"expires_at" => substr((string)($mTok["expires_at"] ?? ""), 0, 10),
|
||||
"scopes" => $scopesArr,
|
||||
"subs" => (int)($mTok["sub_token_count"] ?? 0),
|
||||
], JSON_UNESCAPED_UNICODE), ENT_QUOTES) ?>)'>✏️</button>
|
||||
<?php if (!$mTok['revoked']): ?>
|
||||
<form method="POST" action="index.php#tab-tokens" style="display:inline" onsubmit="return confirm('Master-Token widerrufen? Alle zugehörigen Sub-Tokens werden sofort kaskadierend mit gesperrt!');">
|
||||
<?= $csrfField ?>
|
||||
@@ -5399,6 +5472,113 @@ Antwort:
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- =============== Dialog: Master-Token bearbeiten =============== -->
|
||||
<!--
|
||||
Alles ausser dem Tokenwert selbst und der Nummer. Zuvor
|
||||
erzwang ein Schreibfehler im Namen oder ein nachtraeglich
|
||||
gebrauchtes Recht 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.
|
||||
-->
|
||||
<div id="tokenEditModal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.75); backdrop-filter:blur(8px); z-index:9999; align-items:center; justify-content:center; padding:1rem;">
|
||||
<div class="card" style="max-width:820px; width:100%; max-height:90vh; overflow-y:auto; margin:0;">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">✏️ Token bearbeiten — <span id="tokEditName"></span></h2>
|
||||
</div>
|
||||
|
||||
<div style="font-size:0.82rem; color:var(--text-muted); margin-bottom:1rem;">
|
||||
Der <strong>Tokenwert</strong> und die <strong>Nummer</strong>
|
||||
(<code id="tokEditIdText"></code>) bleiben unverändert — auf den
|
||||
Maschinen muss nichts ausgetauscht werden.
|
||||
</div>
|
||||
|
||||
<div id="tokEditSubHinweis" style="display:none; background:rgba(245,166,35,0.10); border-left:3px solid var(--warning); border-radius:8px; padding:0.75rem 1rem; margin-bottom:1rem; font-size:0.83rem;">
|
||||
An diesem Token hängen <strong><span id="tokEditSubCount"></span> Sub-Tokens</strong>.
|
||||
Sie haben ihre Rechte beim Ausstellen bekommen und behalten sie —
|
||||
eine Einschränkung hier wirkt <em>nicht</em> rückwirkend auf sie.
|
||||
Wer auch die Kinder beschneiden will, widerruft sie und stellt sie neu aus.
|
||||
</div>
|
||||
|
||||
<form method="POST" action="index.php#tab-tokens">
|
||||
<?= $csrfField ?>
|
||||
<input type="hidden" name="action" value="edit_master_token">
|
||||
<input type="hidden" name="token_id" id="tokEditId">
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Bezeichnung / Name</label>
|
||||
<input type="text" name="name" id="tokEditNameInput" class="form-input" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Identitätstyp (Owner Type)</label>
|
||||
<select name="owner_type" id="tokEditOwnerType" class="form-input" required>
|
||||
<option value="custom">Custom / Allgemein</option>
|
||||
<option value="project">Projekt-Gebunden</option>
|
||||
<option value="license">Lizenz-Gebunden</option>
|
||||
<option value="host">Host / Infrastruktur Server</option>
|
||||
<option value="dev_agent">Entwickler KI-Agent</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Freie Identität / Host / HWID</label>
|
||||
<input type="text" name="owner_identity" id="tokEditIdentity" class="form-input">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Projekt</label>
|
||||
<select name="project_slug" id="tokEditProject" class="form-input">
|
||||
<option value="">-- Keins (Universal) --</option>
|
||||
<?php foreach ($projects as $p): ?>
|
||||
<option value="<?= e($p['slug']) ?>"><?= e($p['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Umgebung</label>
|
||||
<select name="environment" id="tokEditEnv" class="form-input" required>
|
||||
<option value="all">🌐 Alle Umgebungen</option>
|
||||
<option value="production">🔴 Nur Produktiv</option>
|
||||
<option value="development">🔵 Nur Entwicklung</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Gültig bis (leer = unbefristet)</label>
|
||||
<input type="date" name="expires_at" id="tokEditExpires" class="form-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-top:0.5rem;">
|
||||
<label class="form-label">Lizenzschlüssel (nur bei Lizenz-Bindung)</label>
|
||||
<input type="text" name="license_key" id="tokEditLicense" class="form-input"
|
||||
placeholder="XXXXX-XXXXX-XXXXX-XXXXX-XXXXX">
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-top:1rem;">
|
||||
<label class="form-label">Erlaubte Rechte (Scopes)</label>
|
||||
<p style="font-size:0.75rem; color:var(--text-muted); margin:0.25rem 0 0.6rem 0;">
|
||||
Mindestens eines. Stärkere Rechte schließen die schwächeren ein.
|
||||
</p>
|
||||
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap:0.5rem;">
|
||||
<?php foreach ($scopeChoices as $value => $labels): ?>
|
||||
<label style="display:flex; align-items:flex-start; gap:0.5rem; font-size:0.85rem; cursor:pointer; background:rgba(255,255,255,0.03); border:1px solid var(--border-glass); border-radius:10px; padding:0.6rem 0.75rem;">
|
||||
<input type="checkbox" name="scopes[]" value="<?= e($value) ?>"
|
||||
class="tok-edit-scope" style="margin-top:3px;">
|
||||
<span>
|
||||
<strong><?= e($labels[0]) ?></strong>
|
||||
<div style="font-size:0.75rem; color:var(--text-muted);"><?= e($labels[1]) ?></div>
|
||||
</span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; justify-content:flex-end; gap:0.75rem; margin-top:1.25rem;">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeTokenEdit()">Abbrechen</button>
|
||||
<button type="submit" class="btn">Speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sub-tokens-create" class="subtab-content">
|
||||
<div class="card">
|
||||
<div class="card-header"><h2 class="card-title">➕ Neuen Token erstellen</h2></div>
|
||||
@@ -5470,26 +5650,6 @@ Antwort:
|
||||
</p>
|
||||
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap:0.5rem;">
|
||||
<?php
|
||||
$scopeChoices = [
|
||||
'*' => ['⚠️ Alle Rechte', 'Uneingeschränkt — auch Releases veröffentlichen und Tokens ausstellen. Nicht an einen Agenten geben.'],
|
||||
'bugtracker:manage' => ['⚙️ Bugtracker verwalten', 'Übernehmen, kommentieren, schließen — schließt Lesen und Melden ein'],
|
||||
'bugtracker:report' => ['🐛 Bugtracker melden', 'Nur Einträge anlegen — schließt Lesen ein'],
|
||||
'bugtracker:read' => ['👁️ Bugtracker lesen', 'Nur Abfragen'],
|
||||
'watchdog:ping' => ['🛡️ Watchdog Heartbeat', 'Statusmeldungen senden'],
|
||||
'watchdog:read' => ['📊 Watchdog lesen', 'Monitore und Ereignisse abfragen'],
|
||||
// Die Supervisor-Rechte fehlten hier, obwohl TokenManager
|
||||
// sie seit 2.6.0 kennt. Folge: Ein Werkbank-Token liess
|
||||
// sich ueber die Oberflaeche gar nicht vollstaendig
|
||||
// ausstellen - dc_findings und dc_metrics im MCP-Adapter
|
||||
// liefen ins unauthorized, und der Grund war nirgends zu
|
||||
// sehen.
|
||||
'supervisor:read' => ['🔎 Befunde lesen', 'Supervisor-Befunde abfragen — braucht ein Werkbank-Token für dc_findings'],
|
||||
'supervisor:manage' => ['🔬 Befunde einstufen', 'Befunde abhaken und bewerten — schließt Lesen ein'],
|
||||
'supervisor:report' => ['📡 Befunde melden', 'Nur einliefern, etwa aus einem Prüfwerkzeug — schließt Lesen ein'],
|
||||
'updateservice:read' => ['📦 Updates prüfen', 'Auf neue Releases prüfen'],
|
||||
'updateservice:publish' => ['🚀 Releases veröffentlichen', 'Neue Releases einstellen — schließt Prüfen ein'],
|
||||
'license:dev' => ['🧪 DEV-Lizenz abholen', 'Befristeten Testschlüssel selbst ausstellen — nur für Werkbank-Token auf Testmaschinen'],
|
||||
];
|
||||
foreach ($scopeChoices as $value => $labels): ?>
|
||||
<label style="display:flex; align-items:flex-start; gap:0.5rem; font-size:0.85rem; cursor:pointer; background:rgba(255,255,255,0.03); border:1px solid var(--border-glass); border-radius:10px; padding:0.6rem 0.75rem;">
|
||||
<?php
|
||||
@@ -6109,6 +6269,52 @@ SYSTEM
|
||||
// GET-Parameter - das skaliert, ueberlebt den Auto-Refresh und laesst
|
||||
// sich als Link teilen.
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Token bearbeiten
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Fuellt den Dialog aus der Zeile, die geklickt wurde.
|
||||
*
|
||||
* Die Angaben kommen als JSON aus dem PHP-Teil mit, statt sie
|
||||
* nachzuladen: Sie stehen ohnehin schon in der Tabelle, und ein
|
||||
* zusaetzlicher Endpunkt waere eine weitere Stelle, an der die
|
||||
* Rechtepruefung sitzen muesste.
|
||||
*/
|
||||
function openTokenEdit(tok) {
|
||||
document.getElementById('tokEditId').value = tok.token_id;
|
||||
document.getElementById('tokEditIdText').innerText = tok.token_id;
|
||||
document.getElementById('tokEditName').innerText = tok.name;
|
||||
document.getElementById('tokEditNameInput').value = tok.name;
|
||||
document.getElementById('tokEditOwnerType').value = tok.owner_type;
|
||||
document.getElementById('tokEditIdentity').value = tok.owner_identity || '';
|
||||
document.getElementById('tokEditProject').value = tok.project_slug || '';
|
||||
document.getElementById('tokEditLicense').value = tok.license_key || '';
|
||||
document.getElementById('tokEditEnv').value = tok.environment;
|
||||
document.getElementById('tokEditExpires').value = tok.expires_at || '';
|
||||
|
||||
const gesetzt = new Set(tok.scopes || []);
|
||||
document.querySelectorAll('.tok-edit-scope').forEach(feld => {
|
||||
feld.checked = gesetzt.has(feld.value);
|
||||
});
|
||||
|
||||
// Der Hinweis zu den Sub-Tokens nur dort, wo es welche gibt -
|
||||
// sonst stumpft er ab und wird ueberlesen.
|
||||
const hinweis = document.getElementById('tokEditSubHinweis');
|
||||
if (tok.subs > 0) {
|
||||
document.getElementById('tokEditSubCount').innerText = tok.subs;
|
||||
hinweis.style.display = 'block';
|
||||
} else {
|
||||
hinweis.style.display = 'none';
|
||||
}
|
||||
|
||||
document.getElementById('tokenEditModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeTokenEdit() {
|
||||
document.getElementById('tokenEditModal').style.display = 'none';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Roadmap
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user