feat: first-admin bootstrap across environments (impl of ADR 0001)#29
Merged
Conversation
Design-only PR — no code yet. Captures the decision for how a fresh devslab-kit deployment gets its first administrator, so the dashboard is usable, without leaving a permanent backdoor as the same artifact moves local-dev → staging → production. Key decisions: - Bootstrap is property-driven (devslab.kit.bootstrap.*), OFF by default → a no-config prod deploy provisions nothing. - No fixed default password: blank admin-password generates a random one logged exactly once (GitLab/Jenkins pattern). A literal admin/admin only appears if the operator writes it (i.e. local dev). - Forced password change on first login via a must_change_password flag + a self-service change-password endpoint; the dashboard guards every route until rotation. - Profiles stay the consumer's mechanism for toggling the properties, never the kit's trigger — with per-environment config snippets for local / staging / prod. - Idempotent runner + optional prod safety pin (fail-on-default-password-in-prod). - Forward-looking: leaves room for a future first-run setup wizard (GET /bootstrap/status) for interactive installs. Bilingual (en + ko), mirroring the README convention. Includes a 5-PR implementation plan and the alternatives considered. Status: Proposed — awaiting sign-off before implementation.
Implements the property-driven first-admin bootstrap from ADR 0001 so a fresh database can be brought up to a usable dashboard without a permanent backdoor, with behaviour configured per environment rather than baked into the artifact. Backend - identity: must_change_password flag (V11) on the user account, surfaced through CurrentUser, the JWT claim, and the login response. - identity: SelfServicePasswordService + POST /admin/api/v1/auth/change-password (verifies the old password, sets the new one, clears the flag, re-issues a token) — the gate release for a bootstrap admin. - autoconfigure: DevslabKitProperties.Bootstrap (OFF by default) + BootstrapAutoConfiguration + DevslabKitBootstrapRunner. Idempotent provisioning of tenant + PLATFORM_ADMIN role + admin.* permissions + admin user; a blank password generates a strong random one logged once; a prod safety pin refuses a weak password under a prod/production profile. - sample-app: switched onto devslab.kit.bootstrap.* (local-dev shape: admin/admin, must-change-password=false); SampleSeedRunner/Properties removed. Build - sample-app: pin application.mainClass so bootJar / nativeCompile / startScripts resolve. Auto-detection failed in this multi-module setup, which also broke `./gradlew build`; this unblocks it. Tests - SelfServicePasswordServiceTest, JjwtAuthTokenServiceTest, and a sample-app assertion that the bootstrap provisions the admin user on first boot. Docs - README first-run sections (en + ko); ADR 0001 marked Accepted.
The application{} block added in the previous commit failed Kotlin-DSL script
compilation on a clean checkout: the `application` type-safe accessor only
exists when the `application` plugin is declared in `plugins {}`, and sample-app
only pulls it in transitively via the GraalVM-native plugin. My local daemon had
the accessor cached from an earlier run, so it compiled locally but blew up on
CI's fresh environment ("Unresolved reference: application / mainClass").
The pin was unnecessary in the first place — `./gradlew build` resolves the boot
main class fine on its own (main's CI was always green without it). The earlier
"build failed" reading was a misdiagnosis: I had invoked a non-existent task
name (bootJarMainClassName) and mistook "task not found" for a real failure.
Removed the block. Verified with `./gradlew build --no-daemon` (fresh, matches
CI) — BUILD SUCCESSFUL.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements ADR 0001 (first-admin bootstrap across environments). Supersedes #28 — this PR carries the ADR (now Accepted) and the implementation, so there's a single thing to review and merge instead of a stacked design→code pair.
Why
A fresh database has zero accounts, so nobody can log in to create menus, roles, or real admins. This adds an opt-in, property-driven first-admin bootstrap that is OFF by default (a no-config prod deploy provisions nothing — no accidental backdoor) and is configured per environment, not baked into the artifact.
What's in it
Backend
must_change_passwordflag (V11) on the user account → surfaced throughCurrentUser, the JWT claim, and the login response.SelfServicePasswordService+POST /admin/api/v1/auth/change-password— verifies the old password, sets the new one, clears the flag, re-issues a token. This is how a bootstrap admin escapes the forced-rotation gate.DevslabKitProperties.Bootstrap(OFF by default) +BootstrapAutoConfiguration+DevslabKitBootstrapRunner: idempotent provisioning of tenant +PLATFORM_ADMINrole + the fulladmin.*permission set + admin user. Blank password ⇒ strong random, logged once. Prod safety pin refuses a weak password under aprod/productionprofile.sample-appswitched ontodevslab.kit.bootstrap.*(local-dev shape:admin/admin,must-change-password=false);SampleSeedRunner/SampleSeedPropertiesdeleted.Lifecycle this enables: log in with bootstrap creds → forced password change → normal admin → create menus / grant perms / add real admins → remove the bootstrap admin.
Build fix (pre-existing, was blocking CI)
sample-app: pinnedapplication.mainClass. Auto-detection (ResolveMainClassName) does not resolve in this multi-module setup, sobootJar/nativeCompile/startScripts— and therefore./gradlew build— were failing onmain(this is why the GraalVM-native task was never finished). My PR's CI can't go green without it, so it's included here.Tests
SelfServicePasswordServiceTest(change / wrong-old-password / missing-user),JjwtAuthTokenServiceTest(claim round-trip both ways), and asample-appTestcontainers assertion that the bootstrap actually provisions the admin on first boot. Full./gradlew buildis green locally.Docs
Follow-up (separate PR,
devslab-kit-admin-ui)The forced-change router guard + change-password screen wired to the new endpoint. Backend is ready for it (
mustChangePasswordis in the login response + token).Note on the environment
A local file-watcher kept reverting tracked-file edits mid-session; the commit was assembled via the index and its contents verified with
git show. Everything in69b8033is correct.