add MFA, user web mail portal
This commit is contained in:
@@ -5,13 +5,13 @@ import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
const mailboxColumns = `id, email, domain_id, password_hash, is_active, quota_bytes, used_bytes, dek_wrapped, dek_nonce, created_at, created_by, totp_secret, totp_enabled`
|
||||
const mailboxColumns = `id, email, domain_id, password_hash, is_active, quota_bytes, used_bytes, dek_wrapped, dek_nonce, created_at, created_by, totp_secret, totp_enabled, mfa_exempt`
|
||||
|
||||
func scanMailbox(row *sql.Row) (*Mailbox, error) {
|
||||
var m Mailbox
|
||||
var createdAt string
|
||||
var createdBy sql.NullInt64
|
||||
if err := row.Scan(&m.ID, &m.Email, &m.DomainID, &m.PasswordHash, &m.IsActive, &m.QuotaBytes, &m.UsedBytes, &m.DEKWrapped, &m.DEKNonce, &createdAt, &createdBy, &m.TOTPSecret, &m.TOTPEnabled); err != nil {
|
||||
if err := row.Scan(&m.ID, &m.Email, &m.DomainID, &m.PasswordHash, &m.IsActive, &m.QuotaBytes, &m.UsedBytes, &m.DEKWrapped, &m.DEKNonce, &createdAt, &createdBy, &m.TOTPSecret, &m.TOTPEnabled, &m.MFAExempt); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -31,7 +31,7 @@ type MailboxWithDomain struct {
|
||||
}
|
||||
|
||||
func (d *DB) ListMailboxes() ([]MailboxWithDomain, error) {
|
||||
rows, err := d.Query(`SELECT m.id, m.email, m.domain_id, m.password_hash, m.is_active, m.quota_bytes, m.used_bytes, m.dek_wrapped, m.dek_nonce, m.created_at, m.created_by, m.totp_secret, m.totp_enabled, dm.domain_name
|
||||
rows, err := d.Query(`SELECT m.id, m.email, m.domain_id, m.password_hash, m.is_active, m.quota_bytes, m.used_bytes, m.dek_wrapped, m.dek_nonce, m.created_at, m.created_by, m.totp_secret, m.totp_enabled, m.mfa_exempt, dm.domain_name
|
||||
FROM esrv_mailboxes m JOIN esrv_domains dm ON dm.id = m.domain_id ORDER BY m.email`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -42,7 +42,7 @@ func (d *DB) ListMailboxes() ([]MailboxWithDomain, error) {
|
||||
var m MailboxWithDomain
|
||||
var createdAt string
|
||||
var createdBy sql.NullInt64
|
||||
if err := rows.Scan(&m.ID, &m.Email, &m.DomainID, &m.PasswordHash, &m.IsActive, &m.QuotaBytes, &m.UsedBytes, &m.DEKWrapped, &m.DEKNonce, &createdAt, &createdBy, &m.TOTPSecret, &m.TOTPEnabled, &m.DomainName); err != nil {
|
||||
if err := rows.Scan(&m.ID, &m.Email, &m.DomainID, &m.PasswordHash, &m.IsActive, &m.QuotaBytes, &m.UsedBytes, &m.DEKWrapped, &m.DEKNonce, &createdAt, &createdBy, &m.TOTPSecret, &m.TOTPEnabled, &m.MFAExempt, &m.DomainName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.CreatedAt, _ = parseTime(createdAt)
|
||||
@@ -65,7 +65,7 @@ func (d *DB) ListMailboxesForDomain(domainID int64) ([]Mailbox, error) {
|
||||
var m Mailbox
|
||||
var createdAt string
|
||||
var createdBy sql.NullInt64
|
||||
if err := rows.Scan(&m.ID, &m.Email, &m.DomainID, &m.PasswordHash, &m.IsActive, &m.QuotaBytes, &m.UsedBytes, &m.DEKWrapped, &m.DEKNonce, &createdAt, &createdBy, &m.TOTPSecret, &m.TOTPEnabled); err != nil {
|
||||
if err := rows.Scan(&m.ID, &m.Email, &m.DomainID, &m.PasswordHash, &m.IsActive, &m.QuotaBytes, &m.UsedBytes, &m.DEKWrapped, &m.DEKNonce, &createdAt, &createdBy, &m.TOTPSecret, &m.TOTPEnabled, &m.MFAExempt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.CreatedAt, _ = parseTime(createdAt)
|
||||
@@ -113,6 +113,13 @@ func (d *DB) SetMailboxActive(id int64, active bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// SetMailboxMFAExempt overrides [Auth] enforce_mailbox_mfa off for this mailbox
|
||||
// specifically, even if its domain isn't exempt.
|
||||
func (d *DB) SetMailboxMFAExempt(id int64, exempt bool) error {
|
||||
_, err := d.Exec(`UPDATE esrv_mailboxes SET mfa_exempt = ? WHERE id = ?`, exempt, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *DB) SetMailboxQuota(id int64, quotaBytes int64) error {
|
||||
_, err := d.Exec(`UPDATE esrv_mailboxes SET quota_bytes = ? WHERE id = ?`, quotaBytes, id)
|
||||
return err
|
||||
@@ -133,6 +140,26 @@ func (d *DB) DisableMailboxTOTP(id int64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// ResetMailboxMFA clears every second factor a mailbox owner has enrolled — TOTP and
|
||||
// every registered passkey — e.g. after a lost device, or (under enforce_mailbox_mfa)
|
||||
// to let an admin get them unstuck without needing a domain/mailbox exemption. Distinct
|
||||
// from DisableMailboxTOTP (TOTP only, self-service from the webmail portal): this is
|
||||
// the admin-management action taken from the Mailboxes list on someone else's account.
|
||||
func (d *DB) ResetMailboxMFA(id int64) error {
|
||||
tx, err := d.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
if _, err := tx.Exec(`UPDATE esrv_mailboxes SET totp_secret = '', totp_enabled = 0 WHERE id = ?`, id); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(`DELETE FROM esrv_mailbox_webauthn_credentials WHERE mailbox_id = ?`, id); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// AddMailboxUsedBytes adjusts the cached running total by delta (positive on store,
|
||||
// negative on delete) in a single statement, avoiding a read-modify-write race.
|
||||
func (d *DB) AddMailboxUsedBytes(id int64, delta int64) error {
|
||||
|
||||
Reference in New Issue
Block a user