adminapi

package
v1.0.4 Latest Latest
Warning

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

Go to latest
Published: Aug 21, 2026 License: MIT Imports: 31 Imported by: 0

Documentation

Overview

Package adminapi provides the admin gateway HTTP handlers, middleware, and server-rendered frontend for The Vault's RBAC admin interface.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func EnsureFirstAdmin

func EnsureFirstAdmin(
	ctx context.Context,
	admins repository.AdminUserRepository,
	marker repository.AdminConfigRepository,
	pepper string,
) error

EnsureFirstAdmin creates a super_admin account on first boot if no admins exist. The password is generated randomly and handed to the operator through firstboot.Deliver, which is never the process log. pepper is applied to the password hash (must match the value the gateway runtime uses; empty for no pepper).

Delivery happens before the row is written, and a failed delivery abandons the bootstrap. The other order is unrecoverable: auth.admin_users is non-empty from the moment Create succeeds, so no later boot mints another, and the deployment owns a super_admin whose password nobody holds and which no admin plane can reset.

Bootstrap happens once per deployment, not once per empty table (F-16). The old gate was admins.Count(ctx) == 0, and auth.admin_users can return to empty: AdminUserRepo.Revoke is a hard DELETE rather than a disable, and RevokeAdmin refuses only self-revocation, so two concurrent super_admin sessions revoking each other empty it — as does anything reaching the database as vault_admin. The next restart then minted a second bootstrap super_admin, with migration 016's created_by-NULL carve-out reopening alongside it, which is precisely the window migration 023 argues can never reopen.

marker is auth.admin_config, which vault_admin may write and which survives the admin table being emptied. The residual risk is honest and smaller than what it replaces: reopening the window now requires both emptying auth.admin_users and blanking this row, and vault_app can do neither on its own. Closing it outright wants an INSERT ... WHERE NOT EXISTS in the repository and a refusal to revoke the last super_admin in the handler.

func GetAdmin

func GetAdmin(ctx context.Context) *model.AdminUser

GetAdmin extracts the admin user from context.

func GetSession

func GetSession(ctx context.Context) *model.AdminSession

GetSession extracts the admin session from context.

func LocalOnly

func LocalOnly(killswitch bool, auditRepo repository.AuditRepository) func(http.Handler) http.Handler

LocalOnly middleware rejects any request where RemoteAddr is not a loopback address. This is layer 4 of the 6-layer local-only enforcement (on top of bind address, hostNetwork, and mTLS).

When killswitch is enabled (default), a non-loopback request triggers a panic that crashes the pod — a hard crash signals a security breach and triggers CrashLoopBackOff for immediate visibility. When disabled (dev mode), it returns 403.

func MaxBody

func MaxBody(maxBytes int64) func(http.Handler) http.Handler

MaxBody limits request body size.

func NewRouter

func NewRouter(auth *AuthHandler, api *Handler, opts ...RouterOpts) http.Handler

NewRouter creates the admin gateway HTTP mux with all routes and middleware.

func RBACCheck

func RBACCheck(perm rbac.Permission, auditLog *audit.Logger) func(http.Handler) http.Handler

RBACCheck middleware enforces that the authenticated admin has the required permission. A permission denial is written to the append-only audit log as an admin_authz_denied event (ASVS V16.3.2): the decision is enforced regardless, and the record is what makes privilege-boundary probing detectable after the fact. auditLog may be nil, in which case the denial is enforced but not recorded — the wired gateway always supplies one.

func Recovery

func Recovery(next http.Handler) http.Handler

Recovery catches panics and returns 500. Killswitch panics are re-panicked to ensure the pod crashes — Recovery must never swallow a security breach signal.

func RejectProxyHeaders

func RejectProxyHeaders(next http.Handler) http.Handler

RejectProxyHeaders middleware rejects requests containing proxy relay headers. This is layer 6 of the local-only enforcement — even if a reverse proxy somehow reaches the gateway, requests with these headers are rejected.

func RequestID

func RequestID(next http.Handler) http.Handler

RequestID generates a unique request ID and adds it to the response headers. Never trusts client-supplied X-Request-ID — always generates a new one.

func SecurityHeaders

func SecurityHeaders(next http.Handler) http.Handler

SecurityHeaders adds security headers to all responses.

func SessionAuth

func SessionAuth(sessions repository.AdminSessionRepository, admins repository.AdminUserRepository, auditLog *audit.Logger) func(http.Handler) http.Handler

SessionAuth middleware validates admin session tokens from the Authorization header. It looks up the session by SHA-256 hash, checks expiry and revocation, and loads the admin user into context. Each token or session validity failure (missing or malformed Authorization header, an unknown, revoked or expired session, or a session whose admin no longer exists) is written to the append-only audit log as an admin_session_rejected event naming the reason (ASVS V16.3.2): the rejection is enforced regardless, and the record is what makes session-token replay and bogus-token probing detectable after the fact. auditLog may be nil, in which case the rejection is still enforced but not recorded; the wired gateway always supplies one.

func WithAdmin added in v0.6.7

func WithAdmin(ctx context.Context, admin *model.AdminUser) context.Context

WithAdmin attaches an admin user to the context. SessionAuth calls it once a session resolves, and tests that drive handlers directly with a pre-authenticated admin call the same function, so the fixture and the deployment write the same key.

func WithSession added in v0.6.7

func WithSession(ctx context.Context, session *model.AdminSession) context.Context

WithSession attaches an admin session to the context. Same role as WithAdmin but for the session value handlers read via GetSession.

Types

type AuthHandler

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

AuthHandler handles admin authentication (login, logout, TOTP setup).

func NewAuthHandler

func NewAuthHandler(
	admins repository.AdminUserRepository,
	sessions repository.AdminSessionRepository,
	auditLog *audit.Logger,
	masterKey []byte,
	pepper string,
	sessionTTL time.Duration,
	maxFailed int,
	lockoutDur time.Duration,
) *AuthHandler

NewAuthHandler creates a new admin authentication handler. pepper is the optional HMAC-pepper applied to admin passwords; it must match the user-side service's pepper so hash formats stay compatible. Empty = no pepper.

func (*AuthHandler) Login

func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request)

Login handles POST /admin/auth/login. Authenticates with username + password + optional TOTP code. Anti-enumeration: always runs Argon2id even for non-existent users.

func (*AuthHandler) Logout

func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request)

Logout handles POST /admin/auth/logout.

func (*AuthHandler) Status

func (h *AuthHandler) Status(w http.ResponseWriter, r *http.Request)

Status handles GET /admin/status.

func (*AuthHandler) TOTPSetup

func (h *AuthHandler) TOTPSetup(w http.ResponseWriter, r *http.Request)

TOTPSetup handles POST /admin/admins/me/totp/setup.

func (*AuthHandler) TOTPVerify

func (h *AuthHandler) TOTPVerify(w http.ResponseWriter, r *http.Request)

TOTPVerify handles POST /admin/admins/me/totp/verify.

type FrontendHandler

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

FrontendHandler serves the server-rendered HTML admin dashboard.

func NewFrontendHandler

func NewFrontendHandler() *FrontendHandler

NewFrontendHandler creates a new frontend handler with parsed templates. Each page template is parsed independently with the layout to avoid {{define "page-content"}} collisions between pages.

func (*FrontendHandler) AdminsPage

func (f *FrontendHandler) AdminsPage(w http.ResponseWriter, r *http.Request)

AdminsPage serves the admin accounts page.

func (*FrontendHandler) AuditPage

func (f *FrontendHandler) AuditPage(w http.ResponseWriter, r *http.Request)

AuditPage serves the audit log page.

func (*FrontendHandler) ClientsPage

func (f *FrontendHandler) ClientsPage(w http.ResponseWriter, r *http.Request)

ClientsPage serves the service clients page.

func (*FrontendHandler) ConfigPage

func (f *FrontendHandler) ConfigPage(w http.ResponseWriter, r *http.Request)

ConfigPage serves the config management page.

func (*FrontendHandler) Dashboard

func (f *FrontendHandler) Dashboard(w http.ResponseWriter, r *http.Request)

Dashboard serves the main dashboard page.

func (*FrontendHandler) KeysPage

func (f *FrontendHandler) KeysPage(w http.ResponseWriter, r *http.Request)

KeysPage serves the key management page.

func (*FrontendHandler) LoginPage

func (f *FrontendHandler) LoginPage(w http.ResponseWriter, r *http.Request)

LoginPage serves the login page.

func (*FrontendHandler) ServeStatic

func (f *FrontendHandler) ServeStatic(w http.ResponseWriter, r *http.Request)

ServeStatic serves embedded static files (CSS, JS).

func (*FrontendHandler) SessionsPage

func (f *FrontendHandler) SessionsPage(w http.ResponseWriter, r *http.Request)

SessionsPage serves the session management page.

func (*FrontendHandler) TOTPSetupPage

func (f *FrontendHandler) TOTPSetupPage(w http.ResponseWriter, r *http.Request)

TOTPSetupPage serves the TOTP enrollment page.

func (*FrontendHandler) UserDetailPage

func (f *FrontendHandler) UserDetailPage(w http.ResponseWriter, r *http.Request)

UserDetailPage serves the user detail page.

func (*FrontendHandler) UsersPage

func (f *FrontendHandler) UsersPage(w http.ResponseWriter, r *http.Request)

UsersPage serves the users management page.

type Handler

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

Handler handles admin API endpoints.

func NewHandler

NewHandler creates a new admin API handler. pepper is the optional HMAC-pepper applied to admin password hashes (must match the user-side service for hash-format parity; empty = none).

func (*Handler) ClearPasswordReset added in v1.0.3

func (h *Handler) ClearPasswordReset(w http.ResponseWriter, r *http.Request)

ClearPasswordReset handles POST /admin/users/{id}/clear-password-reset. It withdraws a forced password reset, returning the account to the ordinary password gate.

It revokes nothing, and the asymmetry with the route above is the whole of the reasoning: imposing the flag says what is already issued is not to be trusted, lifting it says the account is ordinary again. Signing the holder out on the way to telling them so would be a containment action attached to the one verb here that is not one.

Lifting restores the ordinary password gate; it does not open it. An account imported with a hash vault42 cannot parse still has no password that verifies, so a mistaken lift leaves that account shut rather than open.

It goes through SetMustResetPassword rather than ClearMustResetPassword, which clears the same column, because that method also stamps updated_at: a column the web server holds and the admin gateway does not, so calling it from here fails the whole statement with 42501. The two statements exist because the two roles hold different grants, not because the two directions differ.

func (*Handler) CreateAdmin

func (h *Handler) CreateAdmin(w http.ResponseWriter, r *http.Request)

CreateAdmin handles POST /admin/admins.

func (*Handler) CreateClient

func (h *Handler) CreateClient(w http.ResponseWriter, r *http.Request)

CreateClient handles POST /admin/clients.

func (*Handler) CreateRole added in v0.8.0

func (h *Handler) CreateRole(w http.ResponseWriter, r *http.Request)

CreateRole handles POST /admin/roles — add a custom (non-reserved) role.

func (*Handler) DeleteConfig

func (h *Handler) DeleteConfig(w http.ResponseWriter, r *http.Request)

DeleteConfig handles DELETE /admin/config/{key}.

func (*Handler) DeleteEmailBranding added in v0.9.0

func (h *Handler) DeleteEmailBranding(w http.ResponseWriter, r *http.Request)

DeleteEmailBranding handles DELETE /admin/email-branding/{app}.

func (*Handler) DeleteEmailTemplate added in v0.9.0

func (h *Handler) DeleteEmailTemplate(w http.ResponseWriter, r *http.Request)

DeleteEmailTemplate handles DELETE /admin/email-templates/{app}/{name}.

func (*Handler) DeleteRole added in v0.8.0

func (h *Handler) DeleteRole(w http.ResponseWriter, r *http.Request)

DeleteRole handles DELETE /admin/roles/{name} — remove a non-reserved role.

func (*Handler) DeleteUser added in v0.8.0

func (h *Handler) DeleteUser(w http.ResponseWriter, r *http.Request)

DeleteUser handles DELETE /admin/users/{id}. It erases the user account (GDPR) with key-recoverable escrow: when a recovery public key is configured the user's email is written to the encrypted, append-only recovery log before the PII is cascade-deleted and the user row is scrubbed and soft-deleted.

func (*Handler) GetClient

func (h *Handler) GetClient(w http.ResponseWriter, r *http.Request)

GetClient handles GET /admin/clients/{id}.

func (*Handler) GetConfig

func (h *Handler) GetConfig(w http.ResponseWriter, r *http.Request)

GetConfig handles GET /admin/config. entries is a key/value object, not a list, so it carries no list envelope; an empty store is an empty object rather than null. Credential-bearing keys (see redactedConfigKeys) are stripped so a viewer-tier reader never receives them.

func (*Handler) GetEmailBranding added in v0.9.0

func (h *Handler) GetEmailBranding(w http.ResponseWriter, r *http.Request)

GetEmailBranding handles GET /admin/email-branding/{app}.

func (*Handler) GetEmailTemplate added in v0.9.0

func (h *Handler) GetEmailTemplate(w http.ResponseWriter, r *http.Request)

GetEmailTemplate handles GET /admin/email-templates/{app}/{name}.

func (*Handler) GetMetrics

func (h *Handler) GetMetrics(w http.ResponseWriter, r *http.Request)

GetMetrics handles GET /admin/metrics.

func (*Handler) GetUser

func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request)

GetUser handles GET /admin/users/{id}.

func (*Handler) ImportUsers added in v0.8.0

func (h *Handler) ImportUsers(w http.ResponseWriter, r *http.Request)

ImportUsers handles POST /admin/users/import — batch-create passwordless, import_pending accounts from a source system (e.g. the legacy platform). Idempotent on email (CreateImported is ON CONFLICT DO NOTHING). Admin-reserved roles are stripped. On first login each imported account is forced through the magic-link reset.

func (*Handler) ListAdmins

func (h *Handler) ListAdmins(w http.ResponseWriter, r *http.Request)

ListAdmins handles GET /admin/admins. Results are paginated via enforced limit/offset query params (default 50, max 100) to bound the response size of an unbounded admin-user set.

func (*Handler) ListClients

func (h *Handler) ListClients(w http.ResponseWriter, r *http.Request)

ListClients handles GET /admin/clients.

func (*Handler) ListEmailBranding added in v0.9.0

func (h *Handler) ListEmailBranding(w http.ResponseWriter, r *http.Request)

ListEmailBranding handles GET /admin/email-branding.

func (*Handler) ListEmailTemplates added in v0.9.0

func (h *Handler) ListEmailTemplates(w http.ResponseWriter, r *http.Request)

ListEmailTemplates handles GET /admin/email-templates (optional ?app= filter).

func (*Handler) ListKeys

func (h *Handler) ListKeys(w http.ResponseWriter, r *http.Request)

ListKeys handles GET /admin/keys.

func (*Handler) ListRoles added in v0.8.0

func (h *Handler) ListRoles(w http.ResponseWriter, r *http.Request)

ListRoles handles GET /admin/roles — the custom roles catalog.

func (*Handler) ListSessions

func (h *Handler) ListSessions(w http.ResponseWriter, r *http.Request)

ListSessions handles GET /admin/sessions.

It lists ADMIN sessions, not user sessions: the live roster of every currently logged-in admin, with each one's source IP and user agent. That is reconnaissance for an attacker holding a lower-tier admin session, which is the stated reason internal/rbac/rbac.go keeps admins:manage at super_admin, so the route is gated on admins:manage rather than the viewer-tier sessions:list it used to take.

Results are paginated via enforced limit/offset query params (default 50, max 100) to bound the response size of an unbounded active-session set.

func (*Handler) ListUsers

func (h *Handler) ListUsers(w http.ResponseWriter, r *http.Request)

ListUsers handles GET /admin/users. Accepts ?q= query param: UUID format → lookup by ID, contains @ → lookup by email.

func (*Handler) LockUser

func (h *Handler) LockUser(w http.ResponseWriter, r *http.Request)

LockUser handles POST /admin/users/{id}/lock.

func (*Handler) PreviewEmailTemplate added in v0.9.0

func (h *Handler) PreviewEmailTemplate(w http.ResponseWriter, r *http.Request)

PreviewEmailTemplate handles POST /admin/email-templates/preview — render a candidate template against sample data without saving or sending. Always returns 200 with a structured result so the admin UI can show either the rendered output or the validation error.

func (*Handler) PutEmailBranding added in v0.9.0

func (h *Handler) PutEmailBranding(w http.ResponseWriter, r *http.Request)

PutEmailBranding handles PUT /admin/email-branding/{app} — create or replace.

func (*Handler) PutEmailTemplate added in v0.9.0

func (h *Handler) PutEmailTemplate(w http.ResponseWriter, r *http.Request)

PutEmailTemplate handles PUT /admin/email-templates/{app}/{name}.

func (*Handler) QueryAudit

func (h *Handler) QueryAudit(w http.ResponseWriter, r *http.Request)

QueryAudit handles GET /admin/audit.

Pagination shares parsePagination with the other admin list endpoints, so one default (50) and one cap (maxListLimit) apply across the whole gateway.

total is the number of entries in the returned window: repository.AuditFilter has no counterpart that counts matches without returning them. The key is fixed here so that adding a true filtered count later changes a value, not the response shape.

func (*Handler) RequirePasswordReset added in v1.0.3

func (h *Handler) RequirePasswordReset(w http.ResponseWriter, r *http.Request)

RequirePasswordReset handles POST /admin/users/{id}/require-password-reset. It imposes a forced password reset on an existing account and terminates the sessions that account already holds.

The revocation is deliberate and is reported rather than performed quietly. POST /auth/refresh does not consult must_reset_password -- the flag gates the password, and a refresh presents a token instead -- so without it a user who is already signed in keeps rotating their family indefinitely and never meets the reset. The route would then refuse a login nobody was about to attempt while the access it was imposed against continued: a control that reports containment it does not deliver, which is the defect LockUser and RevokeAllSessions were each fixed for. The response and the audit row both carry sessions_revoked so the operator reads the blast radius off the answer.

Best-effort, and after the flag is written, for LockUser's reason: the demand has already committed, and failing the request here would tell the operator no reset was imposed when one was.

func (*Handler) RevokeAdmin

func (h *Handler) RevokeAdmin(w http.ResponseWriter, r *http.Request)

RevokeAdmin handles POST /admin/admins/{id}/revoke.

func (*Handler) RevokeAllSessions

func (h *Handler) RevokeAllSessions(w http.ResponseWriter, r *http.Request)

RevokeAllSessions handles POST /admin/sessions/revoke-all.

It revokes every USER's refresh tokens service-wide. That is the break-glass containment for bulk refresh-token theft, and it is what docs/security.md, docs/api.md, the SessionsRevoke permission's own definition in internal/rbac/rbac.go and the mitigation named in tests/attack/atk_authtok_lock_refresh_test.go all describe.

It used to run UPDATE auth.admin_sessions instead. It touched zero rows in auth.refresh_tokens, so the control four documents lean on did not exist: an operator responding to mass token theft pressed it, was told all_sessions_revoked, and nothing was contained. It also handed an operator-tier admin an availability lever over every super_admin, because revoking admin sessions logs the whole admin plane out and SessionsRevoke sits one tier below the destructive permissions precisely because it was believed to revoke user tokens.

Admin sessions are deliberately left alone. Revoking them is a different action with a different blast radius and belongs behind its own permission at super_admin tier, not smuggled into the user-containment control.

func (*Handler) RevokeClient

func (h *Handler) RevokeClient(w http.ResponseWriter, r *http.Request)

RevokeClient handles POST /admin/clients/{id}/revoke.

func (*Handler) RevokeKey

func (h *Handler) RevokeKey(w http.ResponseWriter, r *http.Request)

RevokeKey handles DELETE /admin/keys/{kid}.

func (*Handler) RotateClientSecret

func (h *Handler) RotateClientSecret(w http.ResponseWriter, r *http.Request)

RotateClientSecret handles POST /admin/clients/{id}/rotate.

func (*Handler) RotateKey

func (h *Handler) RotateKey(w http.ResponseWriter, r *http.Request)

RotateKey handles POST /admin/keys/rotate.

func (*Handler) SetAppRoleRepo added in v0.8.0

func (h *Handler) SetAppRoleRepo(r repository.AppRoleRepository)

SetAppRoleRepo wires the custom-roles catalog repository, enabling the /admin/roles endpoints. Optional (nil → those handlers return 503).

func (*Handler) SetEmailRepos added in v0.9.0

func (h *Handler) SetEmailRepos(branding repository.EmailBrandingRepository, templates repository.EmailTemplateRepository, maxTemplateSize int)

SetEmailRepos wires the per-app email branding + template repositories, enabling the /admin/email-branding and /admin/email-templates endpoints. Optional (nil → those handlers return 503). maxTemplateSize caps custom template body size in bytes; <= 0 disables the size check.

func (*Handler) SetErasureService added in v0.8.0

func (h *Handler) SetErasureService(s *service.ErasureService)

SetErasureService wires the account-erasure service, enabling the DELETE /admin/users/{id} endpoint. Optional (nil → that handler returns 503).

func (*Handler) SetIdentityService added in v0.9.0

func (h *Handler) SetIdentityService(s *service.IdentityService)

SetIdentityService wires the identity service so account import can persist a migrated marketing-consent record. Optional: without it, import still creates accounts but drops any marketing preference in the payload rather than storing a preference it cannot attach provenance to.

func (*Handler) UnlockUser

func (h *Handler) UnlockUser(w http.ResponseWriter, r *http.Request)

UnlockUser handles POST /admin/users/{id}/unlock.

func (*Handler) UpdateConfig

func (h *Handler) UpdateConfig(w http.ResponseWriter, r *http.Request)

UpdateConfig handles PUT /admin/config/{key}.

type LoginRateLimit

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

LoginRateLimit provides per-IP rate limiting for the login endpoint. Allows maxAttempts per window period. Independent of account lockout.

func NewLoginRateLimit

func NewLoginRateLimit(maxAttempts int, window time.Duration) *LoginRateLimit

NewLoginRateLimit creates a login rate limiter.

func (*LoginRateLimit) Wrap

Wrap wraps a handler with per-IP login rate limiting.

type RouterOpts

type RouterOpts struct {
	// DevMode disables LocalOnly and RejectProxyHeaders middleware
	// for development behind ingress controllers.
	DevMode bool

	// Killswitch enables the killswitch panic on non-loopback requests (default: true).
	// When enabled, the pod crashes on breach attempts. When disabled, returns 403.
	Killswitch bool

	// AuditRepo is used by the killswitch to log breach attempts before crashing.
	AuditRepo repository.AuditRepository
}

RouterOpts configures the admin gateway router.

Jump to

Keyboard shortcuts

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