// Package adminbootstrap safely initializes the first Chorus administrator. package adminbootstrap import ( "context" "database/sql" "errors" "strings" "golang.org/x/crypto/bcrypt" ) const chorusOperatorRoleKey = "chorus_operator" var ( ErrMigrationPrecondition = errors.New("administrator bootstrap prerequisites are unavailable") ErrAccountState = errors.New("administrator account is not eligible for bootstrap") ErrDatabaseUnavailable = errors.New("cannot connect to administrator database") ) // Config contains the external inputs accepted by the bootstrap command. // Password is intentionally held only in memory and is never included in errors. type Config struct { DSN string Username string Password string ResetPassword bool } // Result describes the non-sensitive effect of one bootstrap run. type Result int const ( Created Result = iota + 1 Unchanged PasswordReset ) // LoadConfig reads the command's dedicated environment variables. It does not // accept defaults so an operator must make every bootstrap action explicit. func LoadConfig(lookup func(string) (string, bool)) (Config, error) { read := func(name string) string { value, _ := lookup(name) return value } cfg := Config{ DSN: strings.TrimSpace(read("CHORUS_DSN")), Username: strings.TrimSpace(read("CHORUS_ADMIN_USERNAME")), Password: read("CHORUS_ADMIN_PASSWORD"), } if value, ok := lookup("CHORUS_ADMIN_RESET_PASSWORD"); ok && value != "" { if value != "1" { return Config{}, errors.New("CHORUS_ADMIN_RESET_PASSWORD must be 1 when set") } cfg.ResetPassword = true } return cfg, cfg.Validate() } // Validate rejects inputs that cannot be represented safely in the audited // schema or processed by bcrypt. It never includes values in returned errors. func (c Config) Validate() error { if strings.TrimSpace(c.DSN) == "" { return errors.New("CHORUS_DSN is required") } if c.Username == "" { return errors.New("CHORUS_ADMIN_USERNAME is required") } if len([]byte(c.Username)) > 64 { return errors.New("CHORUS_ADMIN_USERNAME exceeds 64 bytes") } if c.Password == "" { return errors.New("CHORUS_ADMIN_PASSWORD is required") } if len([]byte(c.Password)) > 72 { return errors.New("CHORUS_ADMIN_PASSWORD exceeds 72 bytes") } return nil } // Bootstrap creates an enabled administrator or explicitly resets its password. // Existing records are never repaired implicitly: a disabled, deleted, empty- // password, or role-mismatched account is treated as an operator intervention. func Bootstrap(ctx context.Context, db *sql.DB, cfg Config) (Result, error) { return bootstrap(ctx, db, cfg, chorusOperatorRoleKey) } func bootstrap(ctx context.Context, db *sql.DB, cfg Config, roleKey string) (Result, error) { if err := cfg.Validate(); err != nil { return 0, err } if db == nil { return 0, ErrDatabaseUnavailable } if err := db.PingContext(ctx); err != nil { return 0, ErrDatabaseUnavailable } tx, err := db.BeginTx(ctx, nil) if err != nil { return 0, ErrDatabaseUnavailable } defer tx.Rollback() roleID, err := activeChorusOperatorRole(ctx, tx, roleKey) if err != nil { return 0, err } var ( userID int64 storedHash string status string storedRoleID int64 active bool ) err = tx.QueryRowContext(ctx, ` SELECT user_id, password, status, role_id, deleted_at IS NULL FROM sys_user WHERE username = ? FOR UPDATE`, cfg.Username).Scan(&userID, &storedHash, &status, &storedRoleID, &active) switch { case errors.Is(err, sql.ErrNoRows): hash, hashErr := bcryptHash(cfg.Password) if hashErr != nil { return 0, hashErr } if _, err := tx.ExecContext(ctx, ` INSERT INTO sys_user (username, password, role_id, status) VALUES (?, ?, ?, '2')`, cfg.Username, hash, roleID); err != nil { return 0, ErrDatabaseUnavailable } if err := tx.Commit(); err != nil { return 0, ErrDatabaseUnavailable } return Created, nil case err != nil: return 0, ErrDatabaseUnavailable } if !active || status != "2" || storedRoleID != roleID || storedHash == "" { return 0, ErrAccountState } if !cfg.ResetPassword { if err := tx.Commit(); err != nil { return 0, ErrDatabaseUnavailable } return Unchanged, nil } hash, err := bcryptHash(cfg.Password) if err != nil { return 0, err } result, err := tx.ExecContext(ctx, ` UPDATE sys_user SET password = ?, salt = '', update_by = 0 WHERE user_id = ? AND role_id = ? AND status = '2' AND deleted_at IS NULL`, hash, userID, roleID) if err != nil { return 0, ErrDatabaseUnavailable } rowsAffected, err := result.RowsAffected() if err != nil || rowsAffected != 1 { return 0, ErrAccountState } if err := tx.Commit(); err != nil { return 0, ErrDatabaseUnavailable } return PasswordReset, nil } func activeChorusOperatorRole(ctx context.Context, tx *sql.Tx, roleKey string) (int64, error) { var roleID int64 err := tx.QueryRowContext(ctx, ` SELECT role_id FROM sys_role WHERE role_key = ? AND status = '2' AND deleted_at IS NULL FOR UPDATE`, roleKey).Scan(&roleID) if errors.Is(err, sql.ErrNoRows) { return 0, ErrMigrationPrecondition } if err != nil { return 0, ErrDatabaseUnavailable } return roleID, nil } func bcryptHash(password string) (string, error) { hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return "", errors.New("cannot generate administrator password hash") } return string(hash), nil }