Skip to content

fix(auth): auto-assign bootstrap admin to Administrators group (closes #351) - #393

Merged
cristim merged 1 commit into
feat/multicloud-web-frontendfrom
fix/issue-351-admin-group
May 14, 2026
Merged

fix(auth): auto-assign bootstrap admin to Administrators group (closes #351)#393
cristim merged 1 commit into
feat/multicloud-web-frontendfrom
fix/issue-351-admin-group

Conversation

@cristim

@cristim cristim commented May 14, 2026

Copy link
Copy Markdown
Member

Summary

Closes #351.

ensureAdminUser and ensureAdminUserWithPassword
(internal/database/postgres/migrations/migrate.go) insert admin
rows without populating group_ids. A bootstrap admin (via
ADMIN_EMAIL + ADMIN_PASSWORD_SECRET) ended up with
role='admin' but empty group_ids, so the permissions system saw
no group memberships and group-based features broke.

Migration 000024_seed_default_groups already backfills existing
admins at migration time, but it runs only once. The bootstrap path
fires on every container boot, AFTER migrations are at head, so any
admin inserted by ensureAdminUser bypassed the backfill entirely.

What changed

  • Both ensureAdminUser variants now seed group_ids with the
    Administrators group UUID on INSERT. ON CONFLICT (email) DO NOTHING
    / DO UPDATE WHERE password_hash = '' semantics are preserved so
    an operator's customised group_ids is never overwritten.
  • New helper assignAdminGroupAndWarn runs an idempotent backfill
    on any admin row whose group_ids drifted to empty. The
    DISTINCT(unnest(...)) dedupe makes the UPDATE safe to run on
    every boot. Operator customisation (non-empty group_ids that
    deliberately omits the default admin group) is preserved via the
    cardinality(group_ids) = 0 guard.
  • After the backfill, a defensive SELECT counts admins still
    showing empty group_ids and logs a WARN so operators see
    drift in container logs rather than only via a broken UI. This
    is the "defence-in-depth invariant" described in the issue body.

Notes

  • defaultAdminGroupID is duplicated as a package-private literal
    in migrate.go rather than imported from internal/auth, because
    the migrations package must not depend on the auth package
    (auth depends on the DB, not the reverse). Kept in sync with
    auth.DefaultAdminGroupID and the literal in the 000024
    migration via cross-referencing comments.
  • The DO UPDATE clause in ensureAdminUserWithPassword is
    deliberately NOT extended to touch group_ids - the post-insert
    assignAdminGroupAndWarn handles drift uniformly without
    coupling that semantics to the password-empty WHERE clause.

Test plan

Integration test ensure_admin_user_test.go (build tag
integration) covers five sub-cases against a postgres:16-alpine
testcontainer:

  • Fresh insert (no-password): admin has
    group_ids = [DefaultAdminGroupID].
  • Fresh insert (with-password): same.
  • Post-migration drift repair: simulate an admin inserted
    out-of-band with empty group_ids, run RunMigrations again,
    assert the row self-heals.
  • Idempotency: two consecutive RunMigrations calls do not
    duplicate the admin group ID.
  • Operator customisation preserved: admin manually scoped to a
    custom group (deliberately removing the default admin group)
    survives subsequent boots unchanged.

All five pass locally:

=== RUN   TestEnsureAdminUser_GroupAssignment
--- PASS: TestEnsureAdminUser_GroupAssignment (... fresh insert no password / with password / post-migration drift repair / idempotency / operator customisation preserved)
ok  ...migrations  (integration tag)

go build ./... and go vet ./... clean.

Summary by CodeRabbit

  • Tests

    • Added comprehensive integration test for admin user group assignment validation across multiple scenarios, including fresh installations, self-healing of misconfigured records, duplicate prevention, and custom configuration preservation.
  • Bug Fixes

    • Improved admin user group assignment consistency with automatic self-healing for any pre-existing admin records with missing group membership.

Review Change Stack

ensureAdminUser and ensureAdminUserWithPassword in
internal/database/postgres/migrations/migrate.go insert admin rows
without populating group_ids. A bootstrap admin (via ADMIN_EMAIL +
ADMIN_PASSWORD_SECRET) ended up with role='admin' but empty
group_ids, so the permissions system saw no group memberships and
group-based features (frontend rendering, group-based authorisation)
behaved incorrectly.

Migration 000024_seed_default_groups already backfills existing
admins at migration time, but it runs only once. The bootstrap path
fires on every container boot, AFTER migrations are at head, so any
admin inserted by ensureAdminUser bypassed the backfill entirely.

Fix:

- INSERT statements in both ensureAdminUser variants now seed
  group_ids with the Administrators group UUID
  (00000000-0000-5000-8000-000000000001).
- A new assignAdminGroupAndWarn helper runs an idempotent backfill
  UPDATE after each ensureAdminUser call. It targets any admin row
  whose group_ids drifted to empty (NULL or zero-length) - e.g.
  from an out-of-band manual DB seed - so post-migration drift
  self-heals on the next container boot. The DISTINCT(unnest(...))
  dedupe makes the UPDATE safe to run repeatedly.
- After the backfill, a defensive SELECT counts admins still
  showing empty group_ids and logs a WARN so operators see drift
  in container logs rather than only via a broken UI. This is the
  "defence-in-depth invariant" described in the issue body.
- Operator customisation (non-empty group_ids that deliberately
  omits the default admin group) is preserved - the WHERE clause
  is gated on cardinality(group_ids) = 0.

The defaultAdminGroupID constant is duplicated as a package-private
literal rather than imported from internal/auth, because the
migrations package must not depend on the auth package - auth
depends on the DB, not the reverse.

Integration test (ensure_admin_user_test.go, build tag
'integration') covers five scenarios: fresh insert (no-password),
fresh insert (with-password), post-migration drift repair,
idempotency under repeated boots, and operator-customisation
preservation. All five pass against a postgres:16-alpine test
container.

Closes #351
@coderabbitai

coderabbitai Bot commented May 14, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bb8b459c-8120-4f5f-9f13-8526d0d0c746

📥 Commits

Reviewing files that changed from the base of the PR and between 87070ad and 0e92300.

📒 Files selected for processing (2)
  • internal/database/postgres/migrations/ensure_admin_user_test.go
  • internal/database/postgres/migrations/migrate.go

📝 Walkthrough

Walkthrough

Migration bootstrap now guarantees admin users are seeded with the Administrators group on creation via group_ids column seeding. An idempotent backfill repairs pre-existing admin rows with missing group assignments and logs a WARN invariant check, with comprehensive integration tests validating all scenarios.

Changes

Admin Group Assignment

Layer / File(s) Summary
Admin group seeding and backfill implementation
internal/database/postgres/migrations/migrate.go
defaultAdminGroupID constant defines the Administrators group UUID. ensureAdminUser and ensureAdminUserWithPassword now seed group_ids on INSERT. New assignAdminGroupAndWarn performs idempotent UPDATE to append the group ID to admin rows with empty group_ids (guarded by group existence) and runs a COUNT invariant check that logs a WARN if any admins remain without group membership.
Integration test for group assignment scenarios
internal/database/postgres/migrations/ensure_admin_user_test.go
Test helper queries admin group_ids. Main test covers fresh admin insertion with and without password, drift repair when an admin row is manually seeded with empty group_ids, idempotency across consecutive migration runs, and operator customization preservation when admin group_ids are set to a non-default group.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

bug, priority/p1, severity/high

🐰 The admin hops in, group in tow,
No more empty IDs in the row!
Migrations backfill with care,
Warnings if drift appears there,
Bootstrap now knows where to go! 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: auto-assigning the bootstrap admin to the Administrators group, addressing issue #351.
Linked Issues check ✅ Passed All acceptance criteria from #351 are met: ensureAdminUser variants seed group_ids with DefaultAdminGroupID on INSERT, idempotent backfill via assignAdminGroupAndWarn preserves customizations, runtime invariant check warns on drift, and integration tests validate the corrections.
Out of Scope Changes check ✅ Passed All changes directly address issue #351 requirements: group_ids initialization, idempotent backfill logic, invariant checks, and corresponding integration tests with no extraneous modifications.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-351-admin-group

Comment @coderabbitai help to get the list of available commands and usage tips.

@cristim cristim added priority/p2 Backlog-worthy severity/medium Moderate harm urgency/this-sprint Within the current sprint impact/many Affects most users effort/s Hours type/bug Defect triaged Item has been triaged labels May 14, 2026
@cristim

cristim commented May 14, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented May 14, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Full review triggered.

@cristim

cristim commented May 14, 2026

Copy link
Copy Markdown
Member Author

CR pass 1 result: "No actionable comments were generated in the recent review. 🎉" - zero Actionable items, zero Nitpicks, all 5 pre-merge checks (Description, Title, Linked Issues, Out of Scope, Docstring Coverage) passed. CR loop reaches silence on the first pass.

CI status: pre-commit ✅ success, AWS Sanity ✅ success, Azure Sanity ✅ success.

Ready for human review. Not self-merging per repo policy.

@cristim
cristim merged commit c49627e into feat/multicloud-web-frontend May 14, 2026
4 checks passed
@cristim
cristim deleted the fix/issue-351-admin-group branch May 14, 2026 08:34
@cristim
cristim restored the fix/issue-351-admin-group branch May 19, 2026 23:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/s Hours impact/many Affects most users priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/bug Defect urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant