Vaadin Security, Teil 4.1 – Bootstrap des ersten Administrators: Anwender-Sicht und der adapter-neutrale Kern

Sven Ruppert

Teil 3 hat die Bootstrap-Frage zweimal gestellt — einmal in der Beschreibung der MyLoginView des demo-vaadin-rest-clients, die ihre bootstrapStatus-Prüfung gegen das Backend durchführt, und einmal in der Liste der fünf bewusst offen gelassenen Lücken — und beide Male ohne Antwort. Diese Antwort ist Gegenstand von Teil 4. Sie erfordert keine neue Implementierung. Der gesamte im aktuellen Repository beschriebene Mechanismus für Teil 4 ist bereits in voller Tiefe vorhanden.

Sämtliche im Folgenden vorgestellten Quelltexte sind auf GitHub veröffentlicht und

unter https://3g3.eu/vaadin-security abrufbar.

Was Teil 4 als Ganzes leistet, ist nicht das Schließen einer Implementierungslücke, sondern das Aufschreiben einer Architektur, die der Code seit einigen Wochen trägt — eine Architektur mit vier Aufrufkontexten und einem einzigen InitialAdminBootstrapService. Der vorliegende erste Sub-Teil legt das Fundament: Er stellt die strukturelle Diagnose, zeigt die Vaadin-SetupView als anwendersichtige Eröffnung und beschreibt den adapter-neutralen Service-Kern, den die folgenden Sub-Teile dann von außen nach innen durchleuchten werden. Die Token-Mechanik, der PasswordHasher und die Speicher-Verträge sind Gegenstand von Teil 4.2; die vier konkreten Adapter und die Bilanz folgen in Teil 4.3.

Diagnose: Warum Bootstrap ein eigenes Thema verdient

Eine Vaadin-Anwendung wird erstmals gestartet. Der Browser öffnet die Login-View; ein Anwender steht davor, der noch nichts angelegt hat. Was geschieht? Die Frage scheint trivial — bis sie genauer betrachtet wird. Wer sich an die Login-View aus Teil 1 erinnert, weiß, dass MyLoginView einen LoginForm zeigt und die Vaadin-Login-Authentifizierung an eine MyLoginListener-Implementierung delegiert. Diese ihrerseits wendet sich an einen AuthenticationService aus Teil 2, der in den Demos einen In-Memory-UserStorage mit drei vorpopulierten Benutzern befragt — admin/admin, editor/editor, viewer/viewer. Die Antwort auf die anfängliche Frage ist also: Der Anwender gibt eine dieser Demo-Credentials ein, kommt hinein, und ist Administrator.

Genau diese Antwort ist die Lücke. Sie funktioniert, solange das Repository als Demonstration gelesen wird, in der vorpopulierte Klartext-Benutzer didaktisch akzeptabel sind. Sie scheitert in dem Augenblick, in dem dieselbe Architektur jenseits der Demonstration eingesetzt werden soll. Eine produktive Anwendung wird die Klartext-Benutzer nicht mitliefern. Aus den drei vorpopulierten Benutzern wird ein leerer UserStorage. Aus der erfolgreichen Erstanmeldung wird ein Anmeldeversuch ohne Aussicht auf Erfolg. Der erste Administrator existiert nicht — und kann sich auch nicht selbst anlegen, weil dafür wiederum jemand mit Administrationsrechten am Werk sein müsste. Diese zirkuläre Falle ist das eigentliche Bootstrap-Problem, und sie lässt sich nicht durch geschickte Form-Felder oder Routing-Tricks im Vaadin-Layer auflösen. Es verlangt eine eigene Mechanik mit eigener Sicherheitseigenschaft.

Diese Mechanik muss vier Eigenschaften gleichzeitig erfüllen, von denen keine einzelne entbehrlich ist. Die erste ist die Einmaligkeit: Die Mechanik darf nur so lange existieren, bis der erste Administrator angelegt ist; danach muss sie sich selbst deaktivieren. Eine Bootstrap-Funktion, die nach erfolgreicher Erstanlage weiterhin nutzbar bleibt, ist eine Hintertür. Die zweite Eigenschaft ist Token-Authentizität: Die Mechanik muss prüfen können, dass die anlegende Person tatsächlich der Betreiber des Systems ist und nicht der erste anonyme Browser, der die Setup-Seite findet. Diese Prüfung kann nicht über Benutzername und Kennwort erfolgen, weil noch kein Benutzer existiert; sie benötigt ein Token, das aus einem privilegierten Kontext stammt — der Server-Konsole oder einer durch Dateisystemrechte geschützten Datei. Die dritte Eigenschaft ist Race-Sicherheit: Wenn zwei Browser die Setup-Seite zeitgleich aufrufen, darf kein Browser einen Administrator anlegen. Die Mechanik braucht eine atomare Sequenz aus „prüfe, ob noch keiner existiert”, „lege an” und „setze Status auf initialisiert”. Die vierte Eigenschaft ist Spurenarmut: Das Token darf an keiner Stelle protokolliert, geechot oder in Fehlermeldungen wiederholt werden. Eine Anwendung mit korrekter Bootstrap-Mechanik, aber ungefilterten Logs hätte das Token in einer Log-Datei stehen lassen und damit die Mechanik wertlos gemacht.

Diese vier Eigenschaften sind keine willkürliche Aufzählung. Jede einzelne fehlt im naheliegenden Lösungsversuch und fügt der Bibliothek eine Schicht an Disziplin hinzu. Der nahegelegene Versuch wäre, eine SetupView unter /setup zu legen, die den Benutzernamen und das Kennwort entgegennimmt und einen Eintrag im UserStorage erzeugt. Dieser Versuch erfüllt keine der vier Eigenschaften: Er wäre dauerhaft erreichbar, hätte keine Authentifizierung des Aufrufers, keinen Schutz gegen parallele Aufrufe und würde im Ausgabe-JSON eines REST-Pendants vermutlich noch das angelegte Kennwort echoen. Die Mechanik, die im Repository lebt und in den folgenden Kapiteln behandelt wird, hat genau deshalb mehr Bestandteile, als ein erster Blick erwarten ließe.

Eine Beobachtung zur Einordnung. Bootstrap ist nicht dasselbe wie laufende Authentifizierung, und die in Teil 3 benannte Lücke der Klartext-Demo-Benutzer ist nicht dieselbe Lücke wie die Bootstrap-Lücke. Die laufende Authentifizierung — die Frage, ob das vorgelegte Kennwort eines bereits angelegten Benutzers gegen einen Hash und nicht gegen den Klartext verglichen wird — gehört in die Tagesordnung von Teil 5. Die Bootstrap-Mechanik dagegen behandelt den engeren, aber strukturell schwierigeren Fall: dass noch kein Benutzer existiert und genau ein einziger angelegt werden muss, dem später weitere folgen können. Beide Themen berühren sich am PasswordHasher, der die Bootstrap-Initialisierung und die laufende Authentifizierung gemeinsam übernehmen sollte; sie sind jedoch nicht dasselbe Problem.

Die Vaadin-Welt ist von dieser Bootstrap-Frage doppelt betroffen. Sie ist es einmal als Single-JVM-Anwendung — demo-vaadin mit lokalem UserStorage —, in der die SetupView direkt einen lokalen InitialAdminBootstrapService aufruft. Sie ist ein zweites Mal als REST-Client — demo-vaadin-rest-client — implementiert, in dem eine andere SetupView denselben Service über einen REST-Endpunkt aufruft. Beide Vaadin-Anwendungen tragen ein Formular mit denselben Feldern, dieselber Validierung, dieselber char[]-Disziplin am PasswordField — und unterscheiden sich nur in der einen Zeile, die den Service aufruft. Diese Symmetrie ist der Grund, warum Vaadin im vorliegenden Teil nicht ein Adapter unter vier ist, sondern eine zweifache Anbindung, deren Identität didaktisch trägt.

Damit ist die Diagnose gestellt. Eine produktive Anwendung benötigt eine eigene Bootstrap-Mechanik mit vier Sicherheitseigenschaften. Diese Mechanik lebt im security-core und wird über vier Adapter angesprochen, von denen zwei zur Vaadin-Welt gehören. Was diese Mechanik im Anwendererlebnis ergibt, ist Gegenstand von Kapitel 2, dem SetupView aus dem demo-vaadin. Was hinter ihr lebt, wird in Kapitel 3 vom InitialAdminBootstrapService beleuchtet.

Was der Anwender sieht: die SetupView des Vaadin-Demos

Wer das demo-vaadin zum ersten Mal startet und im Browser http://localhost:8080 öffnet, sieht die MyLoginView nicht. Er sieht die SetupView, weil ein BeforeEnterObserver des Login-Views festgestellt hat, dass kein Administrator angelegt ist, und entsprechend zur Setup-Route umleitet. Diese Erstbegegnung ist die Vaadin-spezifische Antwort auf die Bootstrap-Frage aus Kapitel 1: Statt einer leeren Tür mit zwei Eingabefeldern für nicht existierende Credentials erhält der Anwender ein eindeutig benanntes Setup-Formular mit der nötigen Zahl an Feldern und einem unzweideutigen Auftrag.

Das Formular selbst ist schlicht. Eine H2-Überschrift „Initial administrator setup“ leitet ein; ein Paragraf erklärt in zwei Sätzen, dass das Bootstrap-Token aus der Server-Konsole oder der Token-Datei zu beziehen ist und nicht mit dem Administrator-Kennwort verwechselt werden sollte. Es folgen sechs Eingabefelder in der Reihenfolge ihrer Bedeutung: ein PasswordField für das Bootstrap-Token (pflichtig, nicht echoend), ein TextField für den Benutzernamen (vorbelegt mit admin), zwei PasswordField-Instanzen für das neue Kennwort und seine Bestätigung sowie zwei optionale TextField-Instanzen für den Anzeigenamen und die E-Mail-Adresse. Ein Button mit der LUMO_PRIMARY-Variante schließt das Formular ab. Die Vorbelegung des Benutzernamens auf admin ist eine bewusste Anwenderfreundlichkeit; sie deutet die übliche Konvention an, ohne sie zu erzwingen.

Die Wahl von PasswordField für das Token verdient Beachtung. Token sind keine Kennwörter, aber sie verhalten sich für die Zwecke der Browseranzeige genauso: Sie dürfen nicht in der Adressleiste landen, sie dürfen nicht in den Browserverlauf gelangen, sie dürfen vom über die Schulter blickenden Beobachter nicht gelesen werden. PasswordField löst all dies in einem einzigen Element. Der Hinweistext macht zugleich klar, dass dieses Token nicht das Kennwort ist — eine Verwechslung, die ohne diese Klarstellung wahrscheinlich wäre.

Die Weiche zur Login-View lebt in einem BeforeEnterObserver, der vor dem Anzeigen der Setup-View einmal prüft, ob das System bereits initialisiert ist:

@Override
 public void beforeEnter(BeforeEnterEvent event) {
   if (!BootstrapWiring.instance().stateService().bootstrapRequired()) {
     event.forwardTo(MyLoginView.class);
   }
 }

Vier Zeilen, die das Prinzip der Einmaligkeit aus Kapitel 1 in der UI verankern. Wenn ein Anwender die Setup-Route aufruft, nachdem bereits ein Administrator existiert, sieht er das Formular nicht, sondern wird unmittelbar zur Login-View weitergeleitet. Diese Prüfung wird symmetrisch durch die MyLoginView ergänzt, die ihrerseits zur Setup-View leitet, sobald bootstrapRequired() true zurückgibt; beide Weichen schließen zusammen den Bootstrap-Kreislauf, ohne dass eine der beiden Views eine eigene Routing-Logik enthält. Die BootstrapWiring.instance() ist ein statischer Singleton-Holder, der den BootstrapStateService bei der ersten Anfrage initialisiert; sein Zweck wird in Kapitel 3 behandelt.

Das eigentliche Verhalten liegt im Submit-Pfad. Er übernimmt drei Aufgaben gleichzeitig: clientseitige Eingabevalidierung, char[]-Hygiene am Vaadin-PasswordField und die Übersetzung der sechs versiegelten InitialAdminCreationResult-Varianten in Vaadin-Notification-Aufrufe. Der vollständige Pfad sieht so aus:

private void submit() {
   String token = tokenField.getValue();
   String username = usernameField.getValue();
   String password = passwordField.getValue();
   String confirm = confirmField.getValue();
   if (token == null || token.isBlank() || username == null || username.isBlank()
       || password == null || password.isEmpty()) {
     error("Token, username and password are required.");
     return;
   }
   if (!password.equals(confirm)) {
     error("Passwords do not match.");
     return;
   }
   char[] pwd = password.toCharArray();
   InitialAdminCreationResult result = BootstrapWiring.instance().bootstrapService()
       .createInitialAdmin(new CreateInitialAdminCommand(
           token, username, pwd,
           blankToNull(displayNameField.getValue()),
           blankToNull(emailField.getValue())));
   tokenField.clear();
   passwordField.clear();
   confirmField.clear();
 
   switch (result) {
     case InitialAdminCreationResult.Created created -> {
       success("Administrator '" + created.username() + "' created. You can now log in.");
       UI.getCurrent().navigate(MyLoginView.class);
     }
     case InitialAdminCreationResult.AlreadyInitialized ignored -> {
       info("System already initialized — redirecting to login.");
       UI.getCurrent().navigate(MyLoginView.class);
     }
     case InitialAdminCreationResult.InvalidBootstrapToken ignored ->
         error("Bootstrap token rejected.");
     case InitialAdminCreationResult.PasswordPolicyViolation policy ->
         error(policy.reason() == null ? "Password rejected by policy." : policy.reason());
     case InitialAdminCreationResult.InvalidUsername invalid ->
         error(invalid.reason() == null ? "Invalid username." : invalid.reason());
     case InitialAdminCreationResult.InternalError ignored ->
         error("Internal error during setup. Please retry.");
   }
 }

Vier Beobachtungen zu dieser Methode. Die erste betrifft die clientseitige Vorvalidierung. Sie ist bewusst schmal — sie prüft nur die offensichtlichen Verstöße (leere Felder, nicht übereinstimmende Kennwörter), aber nicht die Token-Authentizität, die Kennwort-Richtlinien oder die Verfügbarkeit des Benutzernamens. Diese Prüfungen leben im InitialAdminBootstrapService und nicht in der View, weil die Wahrheit über sie erst dort entschieden werden kann. Eine View, die selbst Token validiert, müsste das Token-Wissen enthalten — und damit zur Sicherheitsgrenze werden, was sie nicht sein soll.

Die zweite Beobachtung betrifft die char[]-Hygiene. Vaadin liefert das Kennwort über PasswordField.getValue() als String. Diese Eigenschaft ist der einzige strukturelle Unterschied zwischen der Vaadin-Welt und den anderen Adaptern, in denen das Kennwort von Anfang an als char[]-Array durch das System fließt. Die Methode submit() löst diesen Unterschied am letzten Vaadin-Berührungspunkt auf: Sie wandelt den String in ein char[] um, übergibt es an den CreateInitialAdminCommand und überlässt dem Service das Wischen des Arrays. Unmittelbar danach werden tokenField, passwordField und confirmField geleert, sodass Token und Kennwort nicht mehr im Vaadin-UI-State liegen. Diese Disziplin ist bewusst pessimistisch: Die String-Instanzen leben weiter, bis der Garbage Collector sie einsammelt; die Vaadin-Komponenten sind jedoch bereits geleert. Eine vollständige Auflösung des String-Problems ist in der aktuellen Vaadin-API nicht möglich; was die View leisten kann, leistet sie.

Die dritte Beobachtung betrifft den Switch zwischen den sechs InitialAdminCreationResult-Varianten. Es ist erschöpfend: Die sealed-Eigenschaft des Result-Typs aus dem Kern erfordert, dass jede Variante einen Zweig erhält. Eine künftige siebte Variante würde im Vaadin-Code sofort einen Compilerfehler erzeugen — die Bibliothek tritt damit als statischer Vertrag zwischen Kern und Adapter auf, nicht nur als Laufzeitvertrag. Die Übersetzungen sind dabei unterschiedlich tief: „Erfolgreiche Anlage“ und „bereits initialisiert“ führen zur Navigation zum Login-View; die vier Fehlervarianten bleiben im Setup-View und zeigen jeweils eine Notification mit einer Begründung, die im Kern formuliert wurde.

Die vierte Beobachtung betrifft das, was die Methode bewusst unterlässt. Sie protokolliert keine der Eingaben. Sie schreibt das Token nicht in eine Datei. Sie zeigt es weder in einer Fehlermeldung noch bei einer Token-Ablehnung an. Wenn das Token verworfen wird, lautet die Meldung schlicht „Bootstrap token rejected” — ohne Hinweis darauf, was am Token falsch war. Diese Spurenarmut ist die UI-seitige Erfüllung der vierten Sicherheitseigenschaft aus Kapitel 1.

Damit ist die anwendersichtige Hälfte des Vaadin-Bootstraps beschrieben. Was hinter BootstrapWiring.instance().bootstrapService() steckt — der InitialAdminBootstrapService mit seinem ReentrantLock, seinem doppelten Check und seinem versiegelten Result-Typ — ist Gegenstand von Kapitel 3.

Der adapter-neutrale Kern: InitialAdminBootstrapService

Der Aufruf von BootstrapWiring.instance().bootstrapService() aus Kapitel 2 führt zu einer einzigen Klasse, die sämtliche Bootstrap-Logik enthält: dem InitialAdminBootstrapService. Sie lebt im Modul security-core, im Paket bootstrap, und ist der adapterneutrale Mittelpunkt der gesamten Mechanik. Alle vier Adapter dieses Teils — die zwei Vaadin-SetupView-Instanzen, der REST-Endpunkt und das CLI-Subkommando — greifen letztlich auf dieselbe Methodensignatur zu. Das Wissen über Token, Race-Sicherheit, Kennwort-Hashing und Result-Form lebt hier; das Wissen über Browser, HTTP oder Terminal lebt anderswo.

Der Vertrag lässt sich in einem Satz zusammenfassen. Eine einzige öffentliche Methode, createInitialAdmin(CreateInitialAdminCommand), nimmt die fünf Eingabefelder aus dem Setup-Formular als versiegelte Befehlsstruktur entgegen und gibt eine InitialAdminCreationResult-Instanz zurück. Es gibt keine zweite Methode, keinen Statuswert, keinen Out-Parameter. Die fünf Eingabefelder sind Bootstrap-Token, Benutzername, Kennwort als char[], optionaler Anzeigename und optionale E-Mail-Adresse. Die Antwortvarianten benennen alle Ausgänge, die der Vorgang haben kann — von erfolgreicher Anlage bis hin zu jedem konkreten Versagensgrund.

Bevor diese Methode aufgerufen werden kann, muss der Service konstruiert sein. Der Konstruktor verlangt fünf erforderliche Komponenten und akzeptiert zwei optionale Werte: einen BootstrapStateService, der den aktuellen Initialisierungsstatus liefert; einen BootstrapTokenStore, der das gespeicherte Token zur Verifikation lädt; einen AdministratorAccountStore, in den der neu angelegte Administrator geschrieben wird; einen PasswordHasher, der das übergebene char[] zum gespeicherten Hash macht; eine PasswordPolicy, die das Kennwort vor dem Hashing auf Mindestanforderungen prüft. Optional sind ein Duration-Token (Standard: 24 Stunden) und ein Clock (Standard: systemUTC). Diese sieben Komponenten sind die einzigen Komponenten des Services; insbesondere ist keine davon Vaadin-, HTTP- oder CLI-spezifisch. Die in Kapitel 2 gezeigte Vaadin-View, der noch zu zeigende REST-Handler und das CLI-Subkommando aus Kapitel 8 nutzen denselben Service und denselben Satz von sieben Komponenten.

Die Methode createInitialAdmin selbst verfügt über drei sichtbare Disziplinen, die zusammen die vier Sicherheitseigenschaften aus Kapitel 1 erfüllen: die Lock-Disziplin, den doppelten Check gegen Race Conditions und das Wischen des Kennwort-Arrays im Finally-Block. Alle drei werden in einer Sequenz von etwa zwanzig Zeilen sichtbar:

public InitialAdminCreationResult createInitialAdmin(CreateInitialAdminCommand command) {
   Objects.requireNonNull(command, "command");
   try {
     lock.lock();
     try {
       if (!stateService.bootstrapRequired()) {
         return new InitialAdminCreationResult.AlreadyInitialized();
       }
       if (administratorStore.hasAnyAdministrator()) {
         return new InitialAdminCreationResult.AlreadyInitialized();
       }
       Optional<BootstrapToken> stored = tokenStore.load();
       if (stored.isEmpty() || !stored.get().matches(command.bootstrapToken())) {
         return new InitialAdminCreationResult.InvalidBootstrapToken();
       }
       if (stored.get().isExpired(clock.instant(), tokenValidity)) {
         return new InitialAdminCreationResult.InvalidBootstrapToken();
       }
       if (!USERNAME.matcher(command.username()).matches()) {
         return new InitialAdminCreationResult.InvalidUsername("Username does not match allowed pattern.");
       }
       Optional<String> policyViolation = passwordPolicy.violates(command.password());
       if (policyViolation.isPresent()) {
         return new InitialAdminCreationResult.PasswordPolicyViolation(policyViolation.get());
       }
       try {
         String hash = passwordHasher.hash(command.password());
         administratorStore.createAdministrator(new NewAdministrator(
             command.username(), command.displayName(), command.email(), hash));
         tokenStore.invalidate();
         return new InitialAdminCreationResult.Created(command.username());
       } catch (RuntimeException ex) {
         LOGGER.log(Level.SEVERE, "Bootstrap failed", ex);
         return new InitialAdminCreationResult.InternalError();
       }
     } finally {
       lock.unlock();
     }
   } finally {
     Arrays.fill(command.password(), '\0');
   }
 }

Die ReentrantLock-Disziplin ist die unmittelbarste der drei. Sie umschließt die gesamte Prüf- und Anlage-Sequenz, sodass zwei zeitgleich aufrufende Threads — etwa zwei parallele Browser-Aufrufe der Vaadin-Setup-Form, oder ein REST-Aufruf parallel zu einer CLI-Eingabe — niemals beide den Status auf „bereits initialisiert” prüfen, beide ein gültiges Token vorfinden und beide einen Administrator anlegen können. Der zweite Thread wartet, bis der erste die Anlage abgeschlossen und das Token invalidiert hat, und sieht dann beim eigenen Eintritt entweder die AlreadyInitialized-Antwort oder die InvalidBootstrapToken-Antwort. Dass dieser Schutz im Kern und nicht in den vier Adaptern liegt, ist die Voraussetzung dafür, dass auch ein Vaadin-Aufruf mit einem zeitgleichen CLI-Aufruf konsistent abläuft — ein Lock im Vaadin-Adapter würde den CLI-Aufruf nicht erfassen, ein Lock pro Adapter wäre eine Inkonsequenz.

Der doppelte Check gegen bootstrapRequired() und hasAnyAdministrator() wirkt auf den ersten Blick redundant. BootstrapStateService.bootstrapRequired() antwortet ohnehin negativ, sobald ein Administrator existiert; warum also noch einmal administratorStore.hasAnyAdministrator() befragen? Die Antwort liegt in der defensiven Aufgabenteilung. Der BootstrapStateService speichert seinen Status und kann theoretisch in einem Zustand sein, in dem er noch nicht erkannt hat, dass im AdministratorAccountStore über eine andere Schicht bereits ein Administrator angelegt wurde — etwa durch eine Datenbankmigration oder einen direkten SQL-Eintrag in einer produktiven Implementierung. Der zweite Check fragt direkt am Speicher nach und nimmt die Wahrheit von dort. Falls beide Antworten widersprüchlich wären, gewinnt die strengere; die Bibliothek meldet im Zweifel AlreadyInitialized und legt keinen weiteren Administrator an.

Das Wischen des Kennwort-char[] im äußeren Finally-Block schließt die Spur, die das Kennwort im Speicher hinterlässt. Arrays.fill(command.password(), ‘\0’) überschreibt jedes Zeichen des übergebenen Arrays mit dem Null-Zeichen. Diese Hygiene wirkt unabhängig vom Ergebnis: Auch bei AlreadyInitialized oder InvalidBootstrapToken wird das Array gewischt, weil der Aufrufer nach dem Methodenaufruf nicht mehr wissen kann, ob das Kennwort weiterhin benötigt wird, und im Zweifel die strengere Disziplin gilt. Diese Aktion ist die Vollendung der char[]-Hygiene, die in Kapitel 2 an der Vaadin-Form begonnen wurde — die Form übergibt das char[], der Service wischt es; beide Schichten zusammen schließen den Pfad.

Das InitialAdminCreationResult schließt das Bild. Es ist eine versiegelte Schnittstelle mit sechs erlaubten Implementierungen:

public sealed interface InitialAdminCreationResult
     permits InitialAdminCreationResult.Created,
             InitialAdminCreationResult.AlreadyInitialized,
             InitialAdminCreationResult.InvalidBootstrapToken,
             InitialAdminCreationResult.PasswordPolicyViolation,
             InitialAdminCreationResult.InvalidUsername,
             InitialAdminCreationResult.InternalError {
 
   record Created(String username) implements InitialAdminCreationResult {}
   record AlreadyInitialized() implements InitialAdminCreationResult {}
   record InvalidBootstrapToken() implements InitialAdminCreationResult {}
   record PasswordPolicyViolation(String reason) implements InitialAdminCreationResult {}
   record InvalidUsername(String reason) implements InitialAdminCreationResult {}
   record InternalError() implements InitialAdminCreationResult {}
 }

Sechs Varianten, drei davon mit semantischem Wert — Created mit dem angelegten Benutzernamen, PasswordPolicyViolation mit dem Grund, InvalidUsername mit dem Grund —, drei ohne weiteren Inhalt: AlreadyInitialized, InvalidBootstrapToken, InternalError. Die Wahl, dass InvalidBootstrapToken keinen weiteren Grund trägt, ist erneut die Spurenarmut aus Kapitel 1: Wäre ein Grund vorhanden, könnte ein Adapter ihn versehentlich an den Aufrufer ausliefern und damit die Token-Authentizität schwächen. Die Bibliothek macht diese Versuchung systematisch unmöglich, indem sie die Information gar nicht erst weitergibt.

Die Symmetrie zur Vaadin-SetupView aus Kapitel 2 wird damit explizit. Der Switch in der submit-Methode trifft genau auf diese sechs Varianten, und die Compiler-Erschöpfungsprüfung garantiert, dass keine davon übersehen wird. Die submit-Methode enthält jedoch keine eigene Bootstrap-Logik — sie ist lediglich eine Übersetzung des Result-Typs in Notification-Aufrufe. Die eigentliche Mechanik übernimmt der InitialAdminBootstrapService und wirkt auf alle vier Adapter gleichermaßen.

Damit ist der adapterneutrale Kern beschrieben. Was bleibt, ist die Frage, woher das Bootstrap-Token stammt, das im Code als selbstverständliche Voraussetzung auftaucht — wer es erzeugt, wo es gespeichert wird und wann es wieder verschwindet. Diese Mechanik umfasst drei Modi, die sich in ihren operativen Eigenschaften unterscheiden, nicht in ihrer fachlichen Wirkung. Sie ist Gegenstand von Teil 4.2.

Drei Themen erwarten den Leser in Teil 4.2: die Token-Mechanik mit ihren drei Modi und ihrem Lebenszyklus, die PasswordHasher mit ihrer PBKDF2-HMAC-SHA256-Implementierung und die durchgehende char[]-Disziplin, schließlich der AdministratorAccountStore-Vertrag mit seinen zwei Adapter-spezifischen Implementierungen. Diese drei Themen sind die Bestandteile, die der Service-Konstruktor aus Kapitel 3 bereits als selbstverständliche Voraussetzungen aufgelistet hat und die nun ihre eigene Tiefe gewinnen.

Total
0
Shares
Previous Post

Destructoring ist die Zukunft von Javas Encapsulation

Next Post

Vaadin Security, Teil 4.2 – Bootstrap des ersten Administrators: Token-Mechanik, PasswordHasher und Speicher-Verträge

Related Posts