add MFA, user web mail portal
This commit is contained in:
+49
-13
@@ -8,13 +8,13 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const adminUserColumns = `id, username, password_hash, must_change_password, totp_secret, totp_enabled, is_global_admin, created_by, created_at`
|
||||
const adminUserColumns = `id, username, password_hash, must_change_password, must_change_username, totp_secret, totp_enabled, is_global_admin, created_by, created_at`
|
||||
|
||||
func scanAdminUser(row *sql.Row) (*AdminUser, error) {
|
||||
var u AdminUser
|
||||
var createdAt string
|
||||
var createdBy sql.NullInt64
|
||||
if err := row.Scan(&u.ID, &u.Username, &u.PasswordHash, &u.MustChangePassword, &u.TOTPSecret, &u.TOTPEnabled, &u.IsGlobalAdmin, &createdBy, &createdAt); err != nil {
|
||||
if err := row.Scan(&u.ID, &u.Username, &u.PasswordHash, &u.MustChangePassword, &u.MustChangeUsername, &u.TOTPSecret, &u.TOTPEnabled, &u.IsGlobalAdmin, &createdBy, &createdAt); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -42,8 +42,11 @@ const (
|
||||
)
|
||||
|
||||
// SeedDefaultAdminIfEmpty creates the default admin account on a brand-new install
|
||||
// (no admin users yet at all) with must_change_password set, so the default
|
||||
// credentials can never be left in place silently.
|
||||
// (no admin users yet at all) with must_change_password AND must_change_username set,
|
||||
// so the well-known default credentials (username "admin") can never be left in place
|
||||
// silently. This is the one and only place must_change_username is ever set — every
|
||||
// other admin (delegated, or a global admin created via the delegation flow) picks
|
||||
// their own username up front and only needs to set their own password.
|
||||
func (d *DB) SeedDefaultAdminIfEmpty() error {
|
||||
n, err := d.CountAdminUsers()
|
||||
if err != nil {
|
||||
@@ -56,15 +59,19 @@ func (d *DB) SeedDefaultAdminIfEmpty() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = d.CreateAdminUser(DefaultAdminUsername, hash, true)
|
||||
_, err = d.Exec(`INSERT INTO esrv_admin_users (username, password_hash, must_change_password, must_change_username, is_global_admin) VALUES (?, ?, 1, 1, 1)`,
|
||||
DefaultAdminUsername, hash)
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateAdminUser inserts a new global-admin account (full access, no domain
|
||||
// restriction). mustChangePassword should be true for the seeded default account so
|
||||
// it can't keep running on default credentials.
|
||||
// restriction) — used by the delegation flow when a global admin grants another user
|
||||
// global access. mustChangePassword should be true so the admin who set the initial
|
||||
// password isn't the only one who knows it; must_change_username is always false
|
||||
// here, since the account was created with the username the new admin will actually
|
||||
// use (see SeedDefaultAdminIfEmpty for the one exception).
|
||||
func (d *DB) CreateAdminUser(username, passwordHash string, mustChangePassword bool) (int64, error) {
|
||||
res, err := d.Exec(`INSERT INTO esrv_admin_users (username, password_hash, must_change_password, is_global_admin) VALUES (?, ?, ?, 1)`,
|
||||
res, err := d.Exec(`INSERT INTO esrv_admin_users (username, password_hash, must_change_password, must_change_username, is_global_admin) VALUES (?, ?, ?, 0, 1)`,
|
||||
username, passwordHash, mustChangePassword)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -82,7 +89,7 @@ func (d *DB) CreateScopedAdminUser(username, passwordHash string, createdBy int6
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
res, err := tx.Exec(`INSERT INTO esrv_admin_users (username, password_hash, must_change_password, is_global_admin, created_by) VALUES (?, ?, 1, 0, ?)`,
|
||||
res, err := tx.Exec(`INSERT INTO esrv_admin_users (username, password_hash, must_change_password, must_change_username, is_global_admin, created_by) VALUES (?, ?, 1, 0, 0, ?)`,
|
||||
username, passwordHash, createdBy)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -128,7 +135,7 @@ func scanAdminUsers(rows *sql.Rows) ([]AdminUser, error) {
|
||||
var u AdminUser
|
||||
var createdAt string
|
||||
var createdBy sql.NullInt64
|
||||
if err := rows.Scan(&u.ID, &u.Username, &u.PasswordHash, &u.MustChangePassword, &u.TOTPSecret, &u.TOTPEnabled, &u.IsGlobalAdmin, &createdBy, &createdAt); err != nil {
|
||||
if err := rows.Scan(&u.ID, &u.Username, &u.PasswordHash, &u.MustChangePassword, &u.MustChangeUsername, &u.TOTPSecret, &u.TOTPEnabled, &u.IsGlobalAdmin, &createdBy, &createdAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u.CreatedAt, _ = parseTime(createdAt)
|
||||
@@ -214,10 +221,19 @@ func (d *DB) GetAdminUserByID(id int64) (*AdminUser, error) {
|
||||
return scanAdminUser(row)
|
||||
}
|
||||
|
||||
// UpdateAdminCredentials mirrors the forced first-login change: new username,
|
||||
// password hash, and clears must_change_password in one step.
|
||||
// UpdateAdminCredentials mirrors the forced first-login change for the seeded default
|
||||
// admin: new username, password hash, and clears must_change_password/
|
||||
// must_change_username in one step.
|
||||
func (d *DB) UpdateAdminCredentials(id int64, username, passwordHash string) error {
|
||||
_, err := d.Exec(`UPDATE esrv_admin_users SET username = ?, password_hash = ?, must_change_password = 0 WHERE id = ?`, username, passwordHash, id)
|
||||
_, err := d.Exec(`UPDATE esrv_admin_users SET username = ?, password_hash = ?, must_change_password = 0, must_change_username = 0 WHERE id = ?`, username, passwordHash, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateAdminPasswordClearMustChange mirrors the forced first-login change for a
|
||||
// delegated admin: password hash only (the username was already chosen when the
|
||||
// account was created), clearing must_change_password.
|
||||
func (d *DB) UpdateAdminPasswordClearMustChange(id int64, passwordHash string) error {
|
||||
_, err := d.Exec(`UPDATE esrv_admin_users SET password_hash = ?, must_change_password = 0, must_change_username = 0 WHERE id = ?`, passwordHash, id)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -236,6 +252,26 @@ func (d *DB) DisableAdminTOTP(id int64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// ResetAdminMFA clears every second factor an admin has enrolled — TOTP and every
|
||||
// registered passkey — e.g. after a lost device, so they can re-enroll from scratch.
|
||||
// Distinct from DisableAdminTOTP (TOTP only, self-service from /account): this is the
|
||||
// admin-management action a manager takes on someone else's account (see
|
||||
// adminWithManageAccess's delegation rule for who's allowed to).
|
||||
func (d *DB) ResetAdminMFA(id int64) error {
|
||||
tx, err := d.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
if _, err := tx.Exec(`UPDATE esrv_admin_users SET totp_secret = '', totp_enabled = 0 WHERE id = ?`, id); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(`DELETE FROM esrv_webauthn_credentials WHERE user_id = ?`, id); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// --- Sessions ---
|
||||
|
||||
func newSessionToken() string {
|
||||
|
||||
Reference in New Issue
Block a user