security: fix data loss on container restart due to relative paths

Root cause: .env.example used relative paths (./data/...) which resolve
to /app/data/ inside the container instead of the persistent volume at
/data/. This caused the database to be recreated on every container
restart, resetting the admin password to a new initial value.

Fixes:
- .env.example: comment out path settings with clear warning about
  relative paths; Dockerfile already provides correct absolute defaults
- auth: add initial_setup_complete flag in settings table as
  defence-in-depth so EnsureAdmin never re-creates an admin after
  the initial setup, even if the users table is unexpectedly empty
- main: add validateDataPaths() startup check that warns when relative
  container paths are detected (potential data-loss misconfiguration)
- auth_test: extend TestEnsureAdmin to verify the flag prevents
  admin re-creation after user deletion
This commit is contained in:
2026-04-05 19:21:15 +02:00
parent c2d4148de6
commit bb3bf0330f
4 changed files with 95 additions and 5 deletions
+30
View File
@@ -156,12 +156,23 @@ func (s *Service) HasUsers() (bool, error) {
// EnsureAdmin creates a default owner user if no users exist.
// It auto-generates a secure password and returns (created, generatedPassword, error).
// A persistent flag ("initial_setup_complete") is stored in the settings table
// so that an admin account is never re-created after the initial setup, even
// if the users table is unexpectedly empty (e.g. due to a misconfigured volume).
func (s *Service) EnsureAdmin(username, email string) (bool, string, error) {
// Defence-in-depth: if the initial setup was already completed once,
// never auto-create another admin even when the users table is empty.
if s.isInitialSetupComplete() {
return false, "", nil
}
hasUsers, err := s.HasUsers()
if err != nil {
return false, "", err
}
if hasUsers {
// Users exist but no flag yet (upgrade path) set the flag now.
s.markInitialSetupComplete()
return false, "", nil
}
@@ -183,9 +194,28 @@ func (s *Service) EnsureAdmin(username, email string) (bool, string, error) {
if err != nil {
return false, "", err
}
// Mark initial setup as complete so the password is never regenerated.
s.markInitialSetupComplete()
return true, password, nil
}
// isInitialSetupComplete checks whether the initial admin setup has already
// been performed by looking for a flag in the settings table.
func (s *Service) isInitialSetupComplete() bool {
var val string
err := s.db.QueryRow(`SELECT value FROM settings WHERE key = 'initial_setup_complete'`).Scan(&val)
return err == nil && val == "true"
}
// markInitialSetupComplete persists the initial-setup flag in the settings table.
func (s *Service) markInitialSetupComplete() {
s.db.Exec(
`INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES ('initial_setup_complete', 'true', CURRENT_TIMESTAMP)`,
)
}
// generateSecurePassword creates a cryptographically secure random password
func generateSecurePassword(length int) (string, error) {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+20 -1
View File
@@ -236,7 +236,7 @@ func TestEnsureAdmin(t *testing.T) {
t.Fatalf("Expected owner role, got %q", user.Role)
}
// Second call should be no-op
// Second call should be no-op (users exist)
created2, _, err := svc.EnsureAdmin("admin2", "admin2@test.com")
if err != nil {
t.Fatalf("Second EnsureAdmin should not fail: %v", err)
@@ -250,6 +250,25 @@ func TestEnsureAdmin(t *testing.T) {
if err != ErrInvalidCredentials {
t.Fatalf("admin2 should not have been created")
}
// initial_setup_complete flag should be set
if !svc.isInitialSetupComplete() {
t.Fatal("Expected initial_setup_complete flag to be set after EnsureAdmin")
}
// Even if all users are deleted, EnsureAdmin must NOT create a new admin
// because the initial_setup_complete flag is set (defence-in-depth).
_, err = db.Exec(`DELETE FROM users`)
if err != nil {
t.Fatalf("Failed to delete all users: %v", err)
}
created3, _, err := svc.EnsureAdmin("admin3", "admin3@test.com")
if err != nil {
t.Fatalf("Third EnsureAdmin should not fail: %v", err)
}
if created3 {
t.Fatal("EnsureAdmin must not create a user when initial_setup_complete flag is set")
}
}
func TestGetAllUsers(t *testing.T) {