adminx

package
v0.5.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 17, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

Documentation

Overview

Package adminx holds the primitives the admin UI is built from: passwords, one time codes, sessions and the audit log.

Everything here relies on the standard library only. An admin panel guards the production settings of a service, so the fewer moving parts under it, the better.

Index

Constants

View Source
const (
	ActionLogin        = "admin.login"
	ActionLogout       = "admin.logout"
	ActionSettingSet   = "settings.set"
	ActionSettingReset = "settings.reset"
	ActionUserCreate   = "admin.user.create"
	ActionUserUpdate   = "admin.user.update"
	ActionUserDelete   = "admin.user.delete"
)

Actions recorded by the admin panel itself. Project pages use their own names.

View Source
const DefaultSessionTTL = 12 * time.Hour

DefaultSessionTTL is how long a session lives.

View Source
const RoleAdmin = "admin"

RoleAdmin may do everything, including managing users. Other roles are the project's own business: the admin UI checks a page's roles against the ones a user has.

Variables

View Source
var (
	// ErrNoSession is returned when the token is unknown: it was never issued, or the
	// session has been revoked.
	ErrNoSession = errors.New("session not found")

	// ErrSessionExpired is returned when the session is too old.
	ErrSessionExpired = errors.New("the session has expired")
)
View Source
var (
	// ErrInvalidCode is returned when the code does not match the secret.
	ErrInvalidCode = errors.New("invalid one time code")

	// ErrCodeReused is returned when a code has already been used. A one time code that
	// works twice is not one time: an intercepted code would be enough to log in.
	ErrCodeReused = errors.New("the code has already been used")
)
View Source
var ErrCodeRequired = errors.New("a one time code is required")

ErrCodeRequired is returned when the account has two factor authentication and no code was given.

View Source
var ErrNoUser = errors.New("user not found")

ErrNoUser is returned by a repository when there is no such user.

View Source
var ErrUserDisabled = errors.New("the account is disabled")

ErrUserDisabled is returned when a disabled account tries to log in.

View Source
var ErrWrongPassword = errors.New("wrong password")

ErrWrongPassword is returned when the password does not match the hash. It is the same error for a wrong password and for an unknown user, so the answer does not say which accounts exist.

Functions

func HashPassword

func HashPassword(password string) (string, error)

HashPassword hashes a password for storage. The result keeps the scheme, the iteration count and the salt, so a hash stays verifiable after the parameters change.

func NewSessionToken

func NewSessionToken() (token, id string, err error)

NewSessionToken returns the token for the cookie and the id to store. Only the hash is stored, so a database dump does not hand out live sessions.

func NewTOTPSecret

func NewTOTPSecret() (string, error)

NewTOTPSecret returns a fresh secret in the base32 form an authenticator app expects.

func NormalizeEmail

func NormalizeEmail(email string) string

NormalizeEmail is how an address is stored and looked up, so a capital letter in the login form does not create a second account.

func SessionID

func SessionID(token string) string

SessionID is the stored id of a token.

func TOTPCode

func TOTPCode(secret string, at time.Time) (string, error)

TOTPCode returns the code for a moment in time.

func TOTPURI

func TOTPURI(issuer, account, secret string) string

TOTPURI returns the otpauth link an authenticator app reads from a QR code.

func VerifyPassword

func VerifyPassword(hash, password string) error

VerifyPassword checks a password against a stored hash.

func VerifyTOTP

func VerifyTOTP(secret, code string, now time.Time, lastCounter int64) (int64, error)

VerifyTOTP checks a code and returns the counter it was issued for. A code from an earlier or the same counter as lastCounter is refused, which is what stops a code from being used twice; store the returned counter next to the user.

Types

type AuditEntry

type AuditEntry struct {
	ID      int64
	At      time.Time
	Actor   string // the email of the signed in user
	Action  string // what was done, "settings.set"
	Target  string // what it was done to, the setting key
	Details string // what changed, in a form a human reads
	IP      string
}

AuditEntry is one recorded action. The log is append only: it answers who changed what, and an entry that can be edited answers nothing.

type AuditRepo

type AuditRepo interface {
	Add(ctx context.Context, e AuditEntry) error

	// List returns the entries newest first. A zero before means from the beginning;
	// otherwise the entries older than that id are returned.
	List(ctx context.Context, limit int, before int64) ([]AuditEntry, error)
}

AuditRepo stores the log.

type Auth

type Auth struct {

	// Now is the clock. Tests replace it; production leaves it alone.
	Now func() time.Time
	// contains filtered or unexported fields
}

Auth is the sign in of the admin panel: it checks the password and the one time code and issues sessions.

func NewAuth

func NewAuth(users UserRepo, sessions SessionRepo, ttl time.Duration) *Auth

NewAuth creates the service. A zero ttl means DefaultSessionTTL.

func (*Auth) Cleanup

func (a *Auth) Cleanup(ctx context.Context) (int, error)

Cleanup deletes the sessions that have expired.

func (*Auth) CreateUser

func (a *Auth) CreateUser(ctx context.Context, email, password string, roles []string, twoFactor bool) (User, string, error)

CreateUser adds an account and returns it together with the TOTP secret to show once. With twoFactor off the secret is empty.

func (*Auth) Login

func (a *Auth) Login(ctx context.Context, email, password, code, ip, userAgent string) (string, error)

Login checks the credentials and returns the token for the cookie.

An unknown address and a wrong password give the same error, so the answer does not tell whether an account exists.

func (*Auth) Logout

func (a *Auth) Logout(ctx context.Context, token string) error

Logout revokes one session.

func (*Auth) Session

func (a *Auth) Session(ctx context.Context, token string) (User, Session, error)

Session returns the signed in user for a token. An expired session is deleted, so a stale cookie does not keep a row alive.

func (*Auth) SetPassword

func (a *Auth) SetPassword(ctx context.Context, id int64, password string) error

SetPassword changes a password and revokes every session of that user: after a password change the old browsers must sign in again.

type MemoryStore

type MemoryStore struct {
	// contains filtered or unexported fields
}

MemoryStore keeps accounts, sessions and the log in memory. It is what tests of the admin panel and of project pages run on, with no database involved.

func NewMemoryStore

func NewMemoryStore() *MemoryStore

NewMemoryStore creates an empty store.

func (*MemoryStore) Audit

func (s *MemoryStore) Audit() AuditRepo

Audit returns the store as an AuditRepo.

func (*MemoryStore) ByEmail

func (s *MemoryStore) ByEmail(_ context.Context, email string) (User, error)

ByEmail returns an account by address.

func (*MemoryStore) ByID

func (s *MemoryStore) ByID(_ context.Context, id int64) (User, error)

ByID returns an account by id.

func (*MemoryStore) Create

func (s *MemoryStore) Create(_ context.Context, u User) (User, error)

Create adds an account.

func (*MemoryStore) Delete

func (s *MemoryStore) Delete(_ context.Context, id int64) error

Delete removes an account together with its sessions.

func (*MemoryStore) List

func (s *MemoryStore) List(context.Context) ([]User, error)

List returns the accounts ordered by address.

func (*MemoryStore) Sessions

func (s *MemoryStore) Sessions() SessionRepo

Sessions returns the store as a SessionRepo.

func (*MemoryStore) Update

func (s *MemoryStore) Update(_ context.Context, u User) error

Update replaces an account.

func (*MemoryStore) Users

func (s *MemoryStore) Users() UserRepo

Users returns the store as a UserRepo.

type Session

type Session struct {
	ID        string // hash of the token, not the token itself
	UserID    int64
	CreatedAt time.Time
	ExpiresAt time.Time
	IP        string
	UserAgent string
}

Session is a signed in browser.

type SessionRepo

type SessionRepo interface {
	Create(ctx context.Context, s Session) error
	ByID(ctx context.Context, id string) (Session, error)
	Delete(ctx context.Context, id string) error
	DeleteByUser(ctx context.Context, userID int64) error
	DeleteExpired(ctx context.Context, now time.Time) (int, error)
}

SessionRepo stores the sessions.

type User

type User struct {
	ID           int64
	Email        string
	PasswordHash string
	TOTPSecret   string // empty when two factor authentication is off
	TOTPCounter  int64  // the last used code, so it cannot be used again
	Roles        []string
	Disabled     bool
	CreatedAt    time.Time
	LastLoginAt  time.Time
}

User is an admin panel account.

func (User) Has

func (u User) Has(role string) bool

Has reports whether the user has a role. An admin has every role.

func (User) HasAny

func (u User) HasAny(roles []string) bool

HasAny reports whether the user has at least one of the roles. No roles means the page is open to any signed in user.

func (User) TwoFactor

func (u User) TwoFactor() bool

TwoFactor reports whether the account has two factor authentication.

type UserRepo

type UserRepo interface {
	ByEmail(ctx context.Context, email string) (User, error)
	ByID(ctx context.Context, id int64) (User, error)
	Create(ctx context.Context, u User) (User, error)
	Update(ctx context.Context, u User) error
	List(ctx context.Context) ([]User, error)
	Delete(ctx context.Context, id int64) error
}

UserRepo stores the accounts.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL