← repoman internals

internal/emergency

import "git.griefed.de/griefed/repoman/internal/emergency"
package emergency // import "git.griefed.de/griefed/repoman/internal/emergency"

Package emergency implements the last-resort password reset for an admin
account, used when every admin has been hard-locked out (e.g. by repeated
failed-login lockouts) and there is no other way back into the system.

It is invoked via `repoman emergency-reset <username>` and runs entirely from
the OS-level CLI without going through the daemon. This means it works even when
the daemon is down or unresponsive, and it requires the caller to already have
OS-level access to the SQLite database file (chmod 600, owned by the service
user).

The command:
 1. Verifies the named account is an admin (operators and read-only users go
    through the normal `repoman user reset-password` flow which requires an
    authenticated admin).
 2. Generates a 24-character cryptographically random password.
 3. Hashes it with the application's pepper and writes it to the DB along with
    must_change_password=1.
 4. Clears all lockout state and reactivates the account.
 5. Optionally clears 2FA secrets if --reset-2fa is passed (with double
    confirmation, since this is a significant trust action).
 6. Deletes all sessions for the user so any prior login is invalidated.
 7. Prints the new password ONCE on stdout and writes a security log + audit log
    entry.

VARIABLES

var ErrNotAdmin = errors.New("emergency reset is only available for admin accounts")
    ErrNotAdmin is returned by Reset when the named user is not an admin.
    Operators and read-only accounts must use the normal authenticated-admin
    reset path; emergency reset would otherwise be a privilege-escalation
    footgun (a sysadmin could quietly change an operator's password without the
    audit trail of going through the daemon).


TYPES

type Confirmer func(prompt, expected string) (bool, error)
    Confirmer prompts the operator for a confirmation string. Returns true
    if the operator typed the expected confirmation. Used by Reset before any
    destructive action.

    The function is injected rather than calling fmt.Scanln directly so that the
    cmd/repoman layer can wire stdin/stdout while the emergency package stays
    unit-testable.

type Options struct {
	// Username of the admin to reset.
	Username string

	// Reset2FA, if true, clears totp_secret and totp_recovery_codes
	// in addition to the password reset. Requires an extra
	// confirmation prompt because losing 2FA enrolment is a higher
	// trust action than just changing the password.
	Reset2FA bool

	// Confirm is called with the warning prompt and expected response.
	// Production wiring uses an interactive stdin reader; tests
	// substitute a deterministic stub.
	Confirm Confirmer

	// Now defaults to time.Now if nil. Tests substitute a fixed clock.
	Now func() time.Time
}
    Options controls the emergency reset behaviour.

type Result struct {
	Username        string
	NewPassword     string
	TwoFactorReset  bool
	SessionsRevoked int
}
    Result is what Reset returns on success. The plaintext password is the
    only place this value ever appears — it is NOT stored anywhere else and the
    caller must surface it to the operator immediately.

func Reset(
	ctx context.Context,
	users auth.UserRepository,
	sessions auth.SessionRepository,
	pepper auth.Pepper,
	secLog *security.Logger,
	opts Options,
) (*Result, error)
    Reset runs the emergency-reset workflow. It mutates the user record,
    invalidates sessions, and returns the new plaintext password to the caller.

    Wiring:
      - users: UserRepository for the read-modify-write of the user row and
        Unlock for clearing lockout state.
      - sessions: SessionRepository.DeleteAllForUser to revoke active sessions.
      - pepper: to hash the new password with the right per-install salt.
      - secLog: security event log; nil disables file logging.