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:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user