feat(platzhalter): Aufloesung, Namensregeln und Shell-Maskierung (#26)
This commit was merged in pull request #26.
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
# #012 M7 – Platzhalter, Namensregeln, Escaping
|
||||
|
||||
**Status:** offen
|
||||
**Status:** erledigt
|
||||
**Meilenstein:** v0.1.0
|
||||
**Labels:** phase-1, backend, security
|
||||
**Geschätzt:** 8 h **Tatsächlich:** –
|
||||
**Geschätzt:** 8 h **Tatsächlich:** 2 h 30 min
|
||||
|
||||
## Ziel
|
||||
|
||||
@@ -11,26 +11,91 @@ Reine, framework-freie Service-Schicht mit dichter Testabdeckung. Hier entstehen
|
||||
|
||||
## Aufgaben
|
||||
|
||||
- [ ] Platzhalter-Auflösung `{{key}}` in Schritt-Titel, Hilfetext und Befehlstext
|
||||
- [ ] Eingebaut aus Gerät und Lauf: `{{hostname}}`, `{{seriennummer}}`, `{{inventarnummer}}`, `{{ticket}}`, `{{datum}}`, `{{techniker}}`; im Modus `DIENSTLEISTUNG` zusätzlich `{{kunde}}` und `{{kundenkuerzel}}`, im Modus `INTERN` leer
|
||||
- [ ] Eingebaut aus dem Mitarbeiter: `{{mitarbeiter}}` (Alias `{{benutzer}}`), `{{vorname}}`, `{{nachname}}`, `{{upn}}`, `{{sam}}`, `{{mail}}`, `{{abteilung}}`, `{{titel}}`, `{{vorgesetzter}}`, `{{vorgesetzter_sam}}`, `{{dn}}`, `{{ou}}`
|
||||
- [ ] Aus der gewählten Abteilung: `{{ou}}` falls nicht vom Mitarbeiter, `{{gruppen}}` kommagetrennt
|
||||
- [ ] `{{ou}}` eines synchronisierten Mitarbeiters ist der Eltern-Container seines `dn` (alles nach dem ersten Komma)
|
||||
- [ ] Dazu die in der Vorlage definierten Variablen; unbekannte Platzhalter bleiben sichtbar stehen
|
||||
- [ ] `{{generiertes_passwort}}`: 20 Zeichen aus Groß, Klein, Ziffern und Sonderzeichen ohne verwechselbare Zeichen, je Anzeige neu erzeugt, **nie** gespeichert; Hinweis „Passwort sofort in den Passwortmanager übernehmen“; in der Druckansicht `[Passwort – nicht gespeichert]`
|
||||
- [ ] Namensregeln (`Einstellung.namensregeln` global, im Modus `DIENSTLEISTUNG` zusätzlich `Kunde.namensregeln` mit Fallback): `upn`, `sam` (höchstens 20 Zeichen), `mail`, `hostname`
|
||||
- [ ] Transliteration (ä→ae, ö→oe, ü→ue, ß→ss, Akzente entfernt), Kleinschreibung, Leerzeichen entfernt, `{nnn}`-Zähler, `{v}` für den ersten Buchstaben des Vornamens, `{kundenkuerzel}`
|
||||
- [ ] Shell-Escaping je Shell: PowerShell (doppelte Anführungszeichen verdoppeln, Backtick maskieren), Bash, Cmd
|
||||
- [ ] HTML-Escaping ersetzter Werte; Markdown im Hilfetext sicher gerendert, kein rohes HTML
|
||||
- [ ] „Alle Befehle als Skript kopieren“ je Abschnitt: alle `BEFEHL`-Schritte in Reihenfolge, getrennt durch Leerzeilen, je mit `# Schritt: <Titel>` davor, Zeilenumbruch passend zur Shell
|
||||
- [x] Platzhalter-Auflösung `{{key}}` in Schritt-Titel, Hilfetext und Befehlstext
|
||||
- [x] Eingebaut aus Gerät und Lauf: `{{hostname}}`, `{{seriennummer}}`, `{{inventarnummer}}`, `{{ticket}}`, `{{datum}}`, `{{techniker}}`; im Modus `DIENSTLEISTUNG` zusätzlich `{{kunde}}` und `{{kundenkuerzel}}`, im Modus `INTERN` leer
|
||||
- [x] Eingebaut aus dem Mitarbeiter: `{{mitarbeiter}}` (Alias `{{benutzer}}`), `{{vorname}}`, `{{nachname}}`, `{{upn}}`, `{{sam}}`, `{{mail}}`, `{{abteilung}}`, `{{titel}}`, `{{vorgesetzter}}`, `{{vorgesetzter_sam}}`, `{{dn}}`, `{{ou}}`
|
||||
- [x] Aus der gewählten Abteilung: `{{ou}}` falls nicht vom Mitarbeiter, `{{gruppen}}` kommagetrennt
|
||||
- [x] `{{ou}}` eines synchronisierten Mitarbeiters ist der Eltern-Container seines `dn` (alles nach dem ersten Komma)
|
||||
- [x] Dazu die in der Vorlage definierten Variablen; unbekannte Platzhalter bleiben sichtbar stehen
|
||||
- [x] `{{generiertes_passwort}}`: 20 Zeichen aus Groß, Klein, Ziffern und Sonderzeichen ohne verwechselbare Zeichen, je Anzeige neu erzeugt, **nie** gespeichert; Hinweis „Passwort sofort in den Passwortmanager übernehmen“; in der Druckansicht `[Passwort – nicht gespeichert]`
|
||||
- [x] Namensregeln (`Einstellung.namensregeln` global, im Modus `DIENSTLEISTUNG` zusätzlich `Kunde.namensregeln` mit Fallback): `upn`, `sam` (höchstens 20 Zeichen), `mail`, `hostname`
|
||||
- [x] Transliteration (ä→ae, ö→oe, ü→ue, ß→ss, Akzente entfernt), Kleinschreibung, Leerzeichen entfernt, `{nnn}`-Zähler, `{v}` für den ersten Buchstaben des Vornamens, `{kundenkuerzel}`
|
||||
- [x] Shell-Escaping je Shell: PowerShell (doppelte Anführungszeichen verdoppeln, Backtick maskieren), Bash, Cmd
|
||||
- [x] HTML-Escaping ersetzter Werte; Markdown im Hilfetext sicher gerendert, kein rohes HTML
|
||||
- [x] „Alle Befehle als Skript kopieren“ je Abschnitt: alle `BEFEHL`-Schritte in Reihenfolge, getrennt durch Leerzeilen, je mit `# Schritt: <Titel>` davor, Zeilenumbruch passend zur Shell
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Test: „Jörg Müller-Lüdenscheid“ ergibt `joerg.mueller-luedenscheid@firma.de` und `jmuellerluedensch` (auf 20 Zeichen gekürzt)
|
||||
- [ ] Test: `{{generiertes_passwort}}` erscheint im angezeigten Befehl, steht aber nicht in `Schrittergebnis`, `Kommentar`, `Protokoll` oder der Druckansicht; zwei Aufrufe liefern verschiedene Passwörter
|
||||
- [ ] Test: ein Wert mit `"` und `` ` `` wird für PowerShell korrekt maskiert und bricht den Befehl nicht
|
||||
- [ ] Test: unbekannte Platzhalter bleiben als `{{key}}` stehen
|
||||
- [ ] Test: `{{ou}}` wird aus `CN=Maria Huber,OU=Buchhaltung,OU=Benutzer,DC=firma,DC=local` zu `OU=Buchhaltung,OU=Benutzer,DC=firma,DC=local`
|
||||
- [ ] Test: Skript-Kopie liefert die Befehle in Abschnittsreihenfolge mit Kommentarzeilen
|
||||
- [ ] Test: im Modus `INTERN` sind `{{kunde}}` und `{{kundenkuerzel}}` leer
|
||||
- [ ] Test: ein Wert mit `<script>` landet HTML-escaped in der Anzeige
|
||||
- [x] Test: „Jörg Müller-Lüdenscheid“ ergibt `joerg.mueller-luedenscheid@firma.de` und `jmuellerluedensch` (17 Zeichen — siehe unten, die Klammer im ursprünglichen Text war falsch)
|
||||
- [x] Test: `{{generiertes_passwort}}` erscheint im angezeigten Befehl, steht aber nicht in `Schrittergebnis`, `Kommentar`, `Protokoll` oder der Druckansicht; zwei Aufrufe liefern verschiedene Passwörter
|
||||
- [x] Test: ein Wert mit `"` und `` ` `` wird für PowerShell korrekt maskiert und bricht den Befehl nicht
|
||||
- [x] Test: unbekannte Platzhalter bleiben als `{{key}}` stehen
|
||||
- [x] Test: `{{ou}}` wird aus `CN=Maria Huber,OU=Buchhaltung,OU=Benutzer,DC=firma,DC=local` zu `OU=Buchhaltung,OU=Benutzer,DC=firma,DC=local`
|
||||
- [x] Test: Skript-Kopie liefert die Befehle in Abschnittsreihenfolge mit Kommentarzeilen
|
||||
- [x] Test: im Modus `INTERN` sind `{{kunde}}` und `{{kundenkuerzel}}` leer
|
||||
- [x] Test: ein Wert mit `<script>` landet HTML-escaped in der Anzeige
|
||||
|
||||
## Beim Bauen aufgefallen
|
||||
|
||||
- **PowerShell liest auch typografische Anführungszeichen als Begrenzer.** `“`, `”` und `„`
|
||||
beenden eine `"…"`-Zeichenkette genauso wie `"`, `‘`, `’`, `‚` und `‛` eine `'…'`-Zeichenkette.
|
||||
Die Maskierung kannte nur das ASCII-Zeichen. Ein Abteilungsname, den jemand aus Word kopiert —
|
||||
Word wandelt `"` von selbst in `“`/`”` —, führte damit fremden Code auf dem Domänencontroller
|
||||
aus. Der Review hat es nicht gelesen, sondern gemessen: er hat sich PowerShell 7 in einem
|
||||
Container geholt und die Markierungsdatei entstehen lassen. Behoben und mit demselben Aufbau
|
||||
gegengeprüft: alle Werte kommen unverändert an, nichts wird ausgeführt.
|
||||
- **Der pwsh-Test hat sich still übersprungen** — dieselbe Falle, die `tests/helpers/db.ts` für die
|
||||
Datenbank längst zugemacht hat: ein stilles Überspringen sieht genauso grün aus wie ein
|
||||
bestandener Lauf. Er nimmt jetzt das Container-Abbild, wenn kein `pwsh` da ist, und bricht in CI
|
||||
ab, statt zu schweigen.
|
||||
- **Zwei verschiedene Passwörter in einer Antwort.** `befehleDesLaufs` hat jeden Schritt zweimal
|
||||
gefüllt — einmal für die Anzeige, einmal für das Skript —, und weil das Passwort bei jedem Füllen
|
||||
neu entsteht, standen auf dem Bildschirm und in der Kopie verschiedene. Der Techniker legt das
|
||||
Konto mit dem einen an und nennt dem Benutzer das andere; weil nichts gespeichert wird, ist es
|
||||
danach unwiederbringlich.
|
||||
- **Eine Vorlagenvariable konnte jeden eingebauten Platzhalter überschreiben.** Eine Variable
|
||||
`techniker` ließ einen Befehl mit fremdem Namen unterschreiben, eine Variable `kunde` holte im
|
||||
Modus Intern die Kundenebene zurück, die es dort ausdrücklich nicht gibt. Gesperrt sind jetzt
|
||||
genau fünf Namen; alles andere bleibt erlaubt, weil eine Onboarding-Vorlage nach `upn`, `sam` und
|
||||
`mail` fragen können muss — die Person steht ja noch nicht im Verzeichnis.
|
||||
- **Die tragende Annahme der Maskierung hat niemand geprüft.** Sie gilt nur, wenn der Platzhalter
|
||||
in der Vorlage in Anführungszeichen steht; `Write-Host {{vorname}}` lässt sich nicht sicher
|
||||
füllen. Das stand als Kommentar da und wurde nirgends eingelöst — der Review hat mit
|
||||
`$(touch …)` eine Datei entstehen lassen. Jetzt wird es geprüft und der Schritt abgewiesen.
|
||||
- **Ich bin selbst in die Falle getappt, die ich in #010 repariert habe:** `$'` ist in
|
||||
`String.replaceAll` ein Rückverweis auf alles hinter dem Treffer. Aus `erste\nzweite` wurde
|
||||
`erste'zweite\n''zweite` — ein Befehl, der nicht mehr zugeht. Gefunden hat es mein eigener Test,
|
||||
der gegen echtes Bash läuft.
|
||||
- **Meine erste HTML-Maskierung war doppelt gemoppelt.** React maskiert Textknoten selbst; eine
|
||||
zweite Maskierung hätte „Forschung & Entwicklung“ sichtbar gemacht. Der Beweis für das
|
||||
Akzeptanzkriterium steht jetzt am gerenderten HTML statt in einer eigenen Funktion.
|
||||
- **`{{ou}}` bei einem maskierten Backslash im DN.** `CN=X\\,OU=Y` ist ein literaler Backslash im
|
||||
Wert, das Komma **ist** der Trenner — die Vorausschau hielt es für maskiert, lieferte nichts, und
|
||||
der Aufrufer fiel still auf die Ziel-OU der Abteilung zurück. Das Konto wäre in der falschen OU
|
||||
gelandet.
|
||||
- **Hostnamen, die Windows ablehnt:** ein Nachname „Con“ reichte, damit die Regel `CON`
|
||||
vorschlägt — das ist die MS-DOS-Konsole. Reservierte Gerätenamen und rein numerische Namen
|
||||
liefern jetzt keinen Vorschlag.
|
||||
- **`!` ist aus dem Passwort geflogen**, und ein Passwort fängt nie mit `= + - @` an: bei
|
||||
eingeschalteter verzögerter Ersetzung frisst `cmd.exe` das `!` still, und die vier anderen liest
|
||||
eine Tabellenkalkulation am Zellenanfang als Formel.
|
||||
|
||||
## Eine Entscheidung, die den Auftragstext korrigiert
|
||||
|
||||
Das Akzeptanzkriterium nennt `jmuellerluedensch` und setzt in Klammern „auf 20 Zeichen gekürzt“.
|
||||
Der Name hat **17** Zeichen, und beides zusammen geht nur mit zwei Regeln, die so jetzt im Code
|
||||
stehen: der Bindestrich fällt im `sam` weg, und eine Regel ohne eigenen `{nnn}` hält drei Stellen
|
||||
für die spätere Unterscheidung frei (20 − 3 = 17). Der Vorrat wird **immer** zurückgehalten, auch
|
||||
solange der Name frei ist — sonst hieße derselbe Mensch `jmuellerluedenscheid`, solange er der
|
||||
Erste ist, und `jmuellerluedensch2`, sobald ein Zweiter dazukommt. Ein einmal notierter
|
||||
Anmeldename muss auch morgen noch aus der Regel herausfallen.
|
||||
|
||||
## Offen geblieben
|
||||
|
||||
- **Cmd ist nur begründet, nicht gemessen.** Es gibt hier kein Windows. Die Ablehnung von `"`, CR,
|
||||
LF und Null-Byte und die Bewertung von `%` und `!` stehen als Begründung im Code — mit dem
|
||||
ausdrücklichen Hinweis, was daran Annahme ist.
|
||||
- **Kein Aufrufer.** Dieser Meilenstein hat keine Oberfläche; die Funktionen werden von #013
|
||||
(Abarbeitung) und #014 (Geräteseite) gerufen. Der Hinweis „Passwort sofort in den
|
||||
Passwortmanager übernehmen“ und die Druckansicht `[Passwort – nicht gespeichert]` sind damit
|
||||
vorbereitet, aber noch nirgends zu sehen.
|
||||
|
||||
Reference in New Issue
Block a user