store

package
v1.6.3 Latest Latest
Warning

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

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

Documentation

Overview

Package store provides a pure-Go SQLite-backed persistence layer.

It uses modernc.org/sqlite which is a CGO-free SQLite implementation, so the whole application can be cross-compiled to a single static binary for Windows/Linux/macOS without a C toolchain.

Index

Constants

View Source
const (
	KindFiring    = "firing"
	KindEscalated = "escalated"
	KindEased     = "eased"
	KindRepeat    = "repeat"
	KindResolved  = "resolved"
)

AlertKind values. Anything level-triggered moves between these; anything edge-triggered stays at KindFiring.

View Source
const (
	// FactorKindTOTP is an authenticator app.
	FactorKindTOTP = "totp"
	// FactorKindPasskey is a WebAuthn credential.
	FactorKindPasskey = "webauthn"
)
View Source
const (
	// BuiltinViewer grants every section, read-only.
	BuiltinViewer = "Viewer"
	// BuiltinOperator grants day-to-day operation but not the sections that
	// confer authority over the installation itself.
	BuiltinOperator = "Operator"
)

Roles are assignable bundles of section grants, so an admin doesn't tick 13 checkboxes per user. A role grants a set of sections, each either read-only or writable; a user's effective access is the union of their roles and their own per-user section list, capped by their read-only flag and by the app-wide disabled sections.

The two built-in roles are read-only in the UI (Duplicate to customise), the same model Templates uses for built-in presets. The `admin` role remains a string on the user rather than a row here: it is the lockout safety valve, and making it data would invite a migration that locks the operator out.

Variables

View Source
var ErrBuiltinRole = errors.New("store: built-in roles cannot be modified")

ErrBuiltinRole is returned when a built-in role is edited or deleted. They are the known-good baseline; the UI offers Duplicate to customise instead.

View Source
var ErrCounterNotAdvanced = errors.New("store: that code's time step was already used")

ErrCounterNotAdvanced means the time step was already spent.

View Source
var ErrDuplicate = errors.New("store: duplicate")

ErrDuplicate is returned when an insert violates a UNIQUE constraint (e.g. a project slug that already exists).

View Source
var ErrLastFactor = errors.New("store: that is the only second factor on this account")

ErrLastFactor is returned when a delete would leave the account with no second factor at all.

View Source
var ErrNotFirstFactor = errors.New("store: this account already has a second factor")

ErrNotFirstFactor means an unauthorised pairing arrived at an account that already has a factor. Adding the first needs no password; adding another does.

View Source
var ErrNotFound = errors.New("store: not found")

ErrNotFound is returned when a lookup yields no row.

View Source
var ErrRoleInUseAsFallback = errors.New("store: role is the configured LDAP fallback")

ErrRoleInUseAsFallback is returned when deleting the role configured as the LDAP fallback. Allowing it would leave the fallback itself dangling — the one thing the fallback exists to prevent.

View Source
var ErrSetupTaken = errors.New("store: setup already completed")

ErrSetupTaken means the first account already existed by the time this insert ran — two setup requests raced and this one lost.

View Source
var ErrTokenNeverForbidden = errors.New("never-expiring MCP tokens are not allowed by the administrator")

ErrTokenNeverForbidden is returned when a never-expiring token is asked for and the policy does not allow one.

View Source
var Sections = []string{
	"dashboard", "containers", "projects", "images", "volumes", "networks", "topology",
	"logs", "events", "alerts", "hosts", "registries", "audit",
}

Sections are the access-control units, matching the app's menu. A user's permissions and the global feature flags are both expressed as sets of these.

Functions

func NormalizeRegistryHost

func NormalizeRegistryHost(host string) string

NormalizeRegistryHost maps the various Docker Hub aliases to a single key so a stored "docker.io" credential matches refs like "nginx" or "user/app".

func ValidSection

func ValidSection(key string) bool

ValidSection reports whether key is a known section.

Types

type APIToken added in v1.4.0

type APIToken struct {
	ID         int64
	UserID     int64
	TokenHash  string
	Name       string
	Sections   []string // empty = inherit all of the user's sections
	HostIDs    []int64  // empty = inherit all of the user's hosts
	ReadOnly   bool
	CreatedAt  time.Time
	LastUsedAt time.Time
	ExpiresAt  time.Time // zero = never expires
	Revoked    bool
}

APIToken is a long-lived bearer credential for programmatic (MCP) access. The plaintext secret is never stored — only TokenHash (a SHA-256 hex digest). A token can only narrow its owner's rights, never widen them:

  • Sections, when non-empty, restricts the token to a subset of the user's granted sections (the dispatcher still intersects with the live user grants, so revoking a section in the admin UI also shrinks the token).
  • HostIDs, when non-empty, restricts the token to a subset of Docker hosts. The local daemon (0) is always reachable, matching the RBAC rule.
  • ReadOnly, when true, forces read-only even if the user is read-write.

func (*APIToken) Expired added in v1.4.0

func (t *APIToken) Expired() bool

Expired reports whether the token has a set expiry that is in the past.

type APITokenWithUser added in v1.4.0

type APITokenWithUser struct {
	APIToken
	Username string
}

APITokenWithUser is an APIToken plus its owner's username, for the admin overview where tokens from every account are listed together.

type AlertDelivery added in v1.6.0

type AlertDelivery struct {
	ID        int64     `json:"id"`
	EventID   int64     `json:"eventId"`
	Channel   string    `json:"channel"` // webhook | email
	Target    string    `json:"target"`  // webhook name + host, or recipients
	OK        bool      `json:"ok"`
	Status    int       `json:"status,omitempty"` // HTTP status, webhooks only
	Detail    string    `json:"detail,omitempty"` // response excerpt or the error
	Attempted time.Time `json:"attemptedAt"`
}

AlertDelivery is one attempt to get an alert out of the building.

type AlertEvent

type AlertEvent struct {
	ID            int64     `json:"id"`
	RuleID        int64     `json:"ruleId"`
	RuleName      string    `json:"ruleName"`
	Type          string    `json:"type"`
	Severity      string    `json:"severity"`
	HostID        int64     `json:"hostId"`
	HostName      string    `json:"hostName"`
	ContainerID   string    `json:"containerId"`
	ContainerName string    `json:"containerName"`
	Message       string    `json:"message"`
	Value         *float64  `json:"value"`
	Acknowledged  bool      `json:"acknowledged"`
	CreatedAt     time.Time `json:"createdAt"`
	// Kind is the point in a condition's life this event marks:
	//
	//	firing    the condition started
	//	escalated it is still on, at a higher severity than before
	//	eased     it is still on, at a lower severity
	//	repeat    it is still on and the re-notify interval elapsed
	//	resolved  it stopped; DurationSec says how long it lasted
	//
	// Edge-triggered rules (state, log, restart) only ever emit "firing" —
	// a container that died or a log line that matched has no later moment at
	// which it stops being true.
	Kind string `json:"kind"`
	// DurationSec is how long the condition held, set on resolved events.
	DurationSec int `json:"durationSec"`
	// AcknowledgedBy names the user who acknowledged it — "someone dealt with
	// this" is only useful if you can ask them about it.
	AcknowledgedBy string     `json:"acknowledgedBy,omitempty"`
	AcknowledgedAt *time.Time `json:"acknowledgedAt,omitempty"`
	// Deliveries is filled in on request, not on every list.
	Deliveries []AlertDelivery `json:"deliveries,omitempty"`
}

AlertEvent is a fired alert recorded for the in-app feed.

type AlertQuery added in v1.6.0

type AlertQuery struct {
	Severity string
	// Severities matches any of several, for callers that mean "the ones that
	// indicate something is wrong" rather than one specific level.
	Severities []string
	Kind       string
	HostID     *int64
	Container  string // substring
	Rule       string // substring
	Text       string // substring of the message
	Unacked    bool
	// HostIDs restricts the query to these hosts; nil means no restriction.
	// Empty-but-non-nil means nothing is visible, which must return no rows
	// rather than all of them — the difference is the whole point of the type.
	HostIDs []int64
	Limit   int
	Offset  int
	// Sort names a column to order by; Desc reverses it. The value is mapped
	// through a fixed whitelist before it reaches SQL — an ORDER BY cannot be a
	// bound parameter, so anything else would be string-building a query out of
	// a query-string value.
	Sort string
	Desc bool
}

AlertQuery filters and pages the event feed. Zero values mean "no filter".

type AlertRule

type AlertRule struct {
	ID        int64  `json:"id"`
	Name      string `json:"name"`
	Enabled   bool   `json:"enabled"`
	Type      string `json:"type"`     // state | resource | log | restart
	Target    string `json:"target"`   // container name substring; ” or '*' = all
	Config    string `json:"config"`   // raw JSON, interpreted by the engine
	Severity  string `json:"severity"` // info | warning | critical
	WebhookID *int64 `json:"webhookId"`
	Email     bool   `json:"email"` // also send this rule by e-mail
	// Emails are this rule's own recipients. Empty falls back to the instance-wide
	// SMTP "To" (and the per-host override), so rules written before per-rule
	// recipients existed keep delivering exactly as they did.
	Emails      []string  `json:"emails"`
	CooldownSec int       `json:"cooldownSec"`
	CreatedAt   time.Time `json:"createdAt"`
}

AlertRule defines when an alert fires and where it goes.

type AlertState added in v1.6.0

type AlertState struct {
	HostID        int64
	HostName      string
	ContainerID   string
	ContainerName string
	Metric        string
	RuleID        int64
	RuleName      string
	Severity      string
	LastValue     *float64
	StartedAt     time.Time
	NotifiedAt    time.Time
}

AlertState is a condition currently held to be true — one row per (host, container, metric), regardless of how many rules noticed it.

type AuditEntry

type AuditEntry struct {
	ID       int64  `json:"id"`
	UserID   int64  `json:"userId"`
	Username string `json:"username"`
	Action   string `json:"action"`
	Target   string `json:"target"`
	Detail   string `json:"detail"`
	IP       string `json:"ip"`
	// HostID is the Docker host the action targeted, 0 for the local daemon or
	// for actions with no host dimension. Recorded because a scoped action is only
	// meaningful with the "where" alongside the "what".
	HostID    int64     `json:"hostId"`
	CreatedAt time.Time `json:"createdAt"`
}

AuditEntry is a single recorded security-relevant action.

type AuthFactor added in v1.6.0

type AuthFactor struct {
	ID          int64
	UserID      int64
	Kind        string
	Name        string
	Secret      string
	LastCounter int64
	CreatedAt   time.Time
	LastUsedAt  time.Time
	// CredentialID identifies a passkey; empty for an authenticator app.
	CredentialID string
	// Credential is the WebAuthn library's own JSON for the key: the public key,
	// the signature counter, and the flags that say what the authenticator proved.
	// Opaque here on purpose — this package should not have opinions about a format
	// the library owns.
	Credential string
}

AuthFactor is one paired second factor: an authenticator app today, a passkey once that lands. The kind is stored rather than implied so both can live in one list — an account's factors are a single set, and splitting them into parallel tables is how "how many factors does this account have?" ends up with two answers.

type ComposeFragment added in v1.4.0

type ComposeFragment struct {
	ID          int64     `json:"id"`
	Name        string    `json:"name"`
	Slug        string    `json:"slug"`
	Description string    `json:"description"`
	Content     string    `json:"content"`
	CreatedBy   string    `json:"createdBy"`
	CreatedAt   time.Time `json:"createdAt"`
}

ComposeFragment is a user-saved "shared definition": a top-level compose fragment (a YAML anchor) merged into builds above services:.

type Grant added in v1.6.0

type Grant struct {
	Granted bool
	Write   bool
	// AllHosts is set when at least one grant of this section came from an
	// unscoped source (an unscoped role, or the account's own section list).
	// Then Hosts is irrelevant.
	AllHosts bool
	// Hosts are the additional in-scope host ids when AllHosts is false. The
	// local daemon (0) is always in scope and is not listed.
	Hosts map[int64]bool
}

Grant is the effective access to one section: whether it's granted at all, whether writes are permitted, and on which hosts.

func (Grant) HasHost added in v1.6.0

func (g Grant) HasHost(hostID int64) bool

HasHost reports whether the grant reaches hostID. Host 0 (the local daemon) is always in scope: it is the one host a single-host install cannot afford to lock itself out of.

type Host

type Host struct {
	ID         int64
	Name       string
	Kind       string
	Address    string
	TLSCA      string
	TLSCert    string
	TLSKey     string
	HostKey    string // pinned SSH host public key (authorized_keys line); ssh hosts only
	AlertEmail string // per-host alert recipient override (falls back to global SMTP To)
	Disabled   bool   // when true the monitor ignores this host (no events/stats)
	CreatedAt  time.Time
}

Host describes a Docker engine endpoint the app can connect to.

Kind is one of:

  • "local": the local daemon (unix socket / windows named pipe)
  • "tcp": a remote daemon over TCP, optionally TLS-secured
  • "ssh": a remote daemon reached through an SSH tunnel

type LDAPConfig

type LDAPConfig struct {
	Enabled      bool   `json:"enabled"`
	URL          string `json:"url"`      // ldap://host:389 or ldaps://host:636
	StartTLS     bool   `json:"startTls"` // upgrade a plain connection to TLS
	BindDN       string `json:"bindDn"`   // service account used to search for users
	BindPassword string `json:"bindPassword"`
	UserBaseDN   string `json:"userBaseDn"`
	UserFilter   string `json:"userFilter"`   // e.g. (uid=%s) or (sAMAccountName=%s)
	AdminGroupDN string `json:"adminGroupDn"` // optional: members are provisioned as admins
	// GroupMappings grant RBAC sections by LDAP group membership. When any are
	// set, LDAP is authoritative for a user's sections (re-synced on each login).
	GroupMappings []LDAPGroupMapping `json:"groupMappings"`
	// FallbackRoleID is granted in place of a mapped role that no longer exists,
	// so deleting a role degrades its members to a known baseline instead of
	// silently leaving them with nothing. 0 means no fallback. It applies only to
	// a mapping that matched and then failed to resolve — never to a user whose
	// groups map to nothing, which would hand access to every account in the
	// directory.
	FallbackRoleID int64 `json:"fallbackRoleId"`
}

LDAPConfig configures optional LDAP / Active Directory authentication. The bind password is encrypted at rest (like the SMTP one) and never returned.

func (LDAPConfig) Configured

func (c LDAPConfig) Configured() bool

Configured reports whether enough is set to attempt LDAP authentication.

type LDAPGroupMapping added in v1.5.0

type LDAPGroupMapping struct {
	GroupDN  string   `json:"groupDn"`
	Sections []string `json:"sections"`
	// omitempty so a config written before roles existed stays truly absent
	// rather than serialising as "roleIds": null.
	RoleIDs []int64 `json:"roleIds,omitempty"`
}

LDAPGroupMapping grants access to members of an LDAP group, matched on the group's full DN. A mapping can hand out named roles, a raw list of sections, or both; a user's effective access is the union over every mapping whose group they belong to. Roles are the intended way to use this; the Sections field predates roles and stays for configs written before they existed.

type MCPTokenPolicy added in v1.6.0

type MCPTokenPolicy struct {
	// DefaultDays is the lifetime applied when the request doesn't name one.
	DefaultDays int `json:"defaultDays"`
	// MaxDays is the longest lifetime a user may choose. 0 means no ceiling.
	//
	// This exists so that "no unlimited tokens" means something. Without a
	// ceiling the rule is a formality: anyone told they cannot have a token that
	// never expires can simply ask for one lasting a hundred years, and the
	// policy has achieved nothing but a longer number.
	MaxDays int `json:"maxDays"`
	// AllowUnlimited permits tokens with no expiry at all.
	AllowUnlimited bool `json:"allowUnlimited"`
}

How long an MCP bearer token is allowed to live.

A token that never expires is a credential nobody ever has to think about again — which is exactly the problem. It outlives the laptop it was pasted into, the contractor it was minted for, and the incident it was involved in. Revocation exists, but revocation requires somebody to remember; an expiry date is the only control here that works when nobody is paying attention.

So the default is 30 days, and never-expiring tokens are off unless an admin turns them back on. This governs what may be MINTED — it deliberately does not touch tokens that already exist, because silently expiring credentials that are in use would break running setups to enforce a policy chosen after they were created. Existing forever-tokens are visible on the MCP admin page and can be revoked there.

func DefaultMCPTokenPolicy added in v1.6.0

func DefaultMCPTokenPolicy() MCPTokenPolicy

DefaultMCPTokenPolicy is what a fresh install gets, and what a stored policy falls back to field by field when it is missing or nonsensical.

func (MCPTokenPolicy) ResolveExpiry added in v1.6.0

func (p MCPTokenPolicy) ResolveExpiry(requestedDays int, never bool, now time.Time) (time.Time, error)

ResolveExpiry turns a request into a concrete expiry time.

"Never" is a separate flag rather than days==0 on purpose. Overloading zero would make the two very different intents — "I did not choose" and "I want this to live forever" — indistinguishable on the wire, and the safe reading of silence has to be the policy default, not immortality.

The whole decision lives here so the handler cannot enforce it slightly differently from anything added later. now is passed in so the tests are not timing-dependent.

type OAuthClient added in v1.4.0

type OAuthClient struct {
	ID           string // client_id
	Name         string
	RedirectURIs []string
	CreatedAt    time.Time
}

OAuthClient is a dynamically-registered (RFC 7591) MCP OAuth client. Clients are public (no secret); security rests on PKCE + exact redirect-URI matching.

type OAuthCode added in v1.4.0

type OAuthCode struct {
	ClientID      string
	UserID        int64
	RedirectURI   string
	CodeChallenge string
	Resource      string
	Scope         string
	ExpiresAt     time.Time
}

OAuthCode is the state bound to a single-use authorization code.

type OAuthRefreshToken added in v1.4.0

type OAuthRefreshToken struct {
	ClientID  string
	UserID    int64
	Scope     string
	Resource  string
	ExpiresAt time.Time
}

OAuthRefreshToken is the state bound to a refresh token.

type ParseRule

type ParseRule struct {
	ID        int64     `json:"id"`
	Name      string    `json:"name"`
	Pattern   string    `json:"pattern"`
	CreatedAt time.Time `json:"createdAt"`
}

ParseRule is a saved log-parsing rule: a regex with named capture groups that the Logs view applies to extract structured fields (columns) from log lines.

type Project added in v1.2.0

type Project struct {
	ID          int64
	Name        string
	Slug        string
	ComposeFile string
	HostID      int64 // target Docker host for deploy; 0 = local daemon
	// AllowRemoteHostPaths lets a remote deploy mount bind sources from OUTSIDE
	// the project folder — paths on the remote host itself, which are otherwise
	// refused because we can't see what they hold. Off by default; enabling it
	// requires the "hosts" permission and is audited.
	AllowRemoteHostPaths bool
	// LastDeployedProfiles is the profile list passed to the last successful
	// `compose up` for this project — what's actually running, as opposed to
	// whatever profiles a user has since selected for the NEXT deploy (that
	// selection is a client-only preference; see web/src/pages/Projects.tsx).
	// nil/empty means never successfully deployed, or deployed with no profiles.
	LastDeployedProfiles []string
	CreatedBy            string
	CreatedAt            time.Time
	UpdatedAt            time.Time
}

Project is a managed compose project: a folder under the data dir holding a compose file plus sidecar config/script files, deployed via the docker compose CLI. The folder is keyed by the numeric ID (derived at runtime, not stored) so renames never move files. Slug is the compose project name.

type ProjectTemplate added in v1.4.0

type ProjectTemplate struct {
	ID          int64     `json:"id"`
	Name        string    `json:"name"`
	Slug        string    `json:"slug"`
	Description string    `json:"description"`
	CreatedBy   string    `json:"createdBy"`
	CreatedAt   time.Time `json:"createdAt"`
}

ProjectTemplate is a user-saved project preset. Only metadata lives in the DB; the scaffold files live on disk under DataDir/project-templates/{id}/.

type Registry

type Registry struct {
	ID        int64
	Name      string
	Address   string
	Username  string
	CreatedAt time.Time
}

Registry holds credentials for a container image registry. The secret (password/token) is encrypted at rest and never returned in listings.

type RegistryAuth

type RegistryAuth struct {
	Address  string
	Username string
	Password string
}

RegistryAuth is the decrypted credential pair used to authenticate to a registry for pull/push. It is only assembled server-side, never serialised.

type Role added in v1.6.0

type Role struct {
	ID          int64         `json:"id"`
	Name        string        `json:"name"`
	Description string        `json:"description"`
	Builtin     bool          `json:"builtin"`
	Sections    []RoleSection `json:"sections"`
	// HostIDs limits the role to those hosts. EMPTY MEANS EVERY HOST, so an
	// existing role keeps its reach and a newly created one isn't accidentally
	// scoped to nothing. The local daemon (0) is always reachable and is never
	// stored here.
	HostIDs []int64 `json:"hostIds"`
}

Role is a named bundle of section grants, optionally limited to a set of Docker hosts.

type RoleSection added in v1.6.0

type RoleSection struct {
	Section string `json:"section"`
	Write   bool   `json:"write"`
}

RoleSection is one section grant inside a role.

type SMTPConfig

type SMTPConfig struct {
	Host     string `json:"host"`
	Port     int    `json:"port"`
	Username string `json:"username"`
	Password string `json:"password"`
	From     string `json:"from"`
	To       string `json:"to"`  // comma-separated recipients
	TLS      bool   `json:"tls"` // implicit TLS (e.g. port 465); otherwise STARTTLS if offered
}

SMTPConfig is the mail server used for the email alert channel. The password is stored encrypted at rest (the persisted JSON holds ciphertext); it is decrypted on read and never returned to API clients.

func (SMTPConfig) Configured

func (c SMTPConfig) Configured() bool

Configured reports whether enough is set to attempt sending.

type ServiceBlock added in v1.4.0

type ServiceBlock struct {
	ID          int64     `json:"id"`
	Name        string    `json:"name"`
	Slug        string    `json:"slug"`
	Description string    `json:"description"`
	Service     string    `json:"service"`
	ServiceYAML string    `json:"serviceYaml"`
	Volumes     []string  `json:"volumes"`
	CreatedBy   string    `json:"createdBy"`
	CreatedAt   time.Time `json:"createdAt"`
}

ServiceBlock is a user-defined builder block — a single compose service fragment stored inline.

type Session added in v1.6.0

type Session struct {
	ID         string // the token's jti
	UserID     int64
	IP         string
	UserAgent  string
	CreatedAt  time.Time
	LastSeenAt time.Time
	ExpiresAt  time.Time
}

Session is one signed-in browser or client.

type Store

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

Store wraps the database handle and exposes typed queries.

func Open

func Open(path string) (*Store, error)

Open opens (creating if necessary) the SQLite database at path and runs all pending migrations. A path of ":memory:" yields an ephemeral DB.

func (*Store) APITokenByHash added in v1.4.0

func (s *Store) APITokenByHash(ctx context.Context, hash string) (*APIToken, error)

APITokenByHash looks up an active (non-revoked) token by its SHA-256 hash. Expiry is NOT enforced here — callers check Expired() so they can treat an expired token identically to a missing one. Returns ErrNotFound if absent or revoked.

func (*Store) AckAlertEvent

func (s *Store) AckAlertEvent(ctx context.Context, id int64, by string) error

AckAlertEvent marks an alert event acknowledged, recording who did it.

func (*Store) AckMatchingAlertEvents added in v1.6.0

func (s *Store) AckMatchingAlertEvents(ctx context.Context, q AlertQuery, by string) (int64, error)

AckMatchingAlertEvents acknowledges every unacknowledged event the filter matches, returning how many changed. Used by "acknowledge all", which is deliberately scoped to what the caller is currently looking at rather than to the whole table.

func (*Store) AdminRevokeAPIToken added in v1.4.0

func (s *Store) AdminRevokeAPIToken(ctx context.Context, id int64) (bool, error)

AdminRevokeAPIToken marks any token revoked regardless of owner — for admins managing the fleet. Unlike RevokeAPIToken it is not scoped to a user. The bool reports whether a matching, still-active token was revoked (false → unknown id or already revoked), so the handler can return 404 instead of a false success.

func (*Store) AlertDeliveriesFor added in v1.6.0

func (s *Store) AlertDeliveriesFor(ctx context.Context, eventIDs []int64) (map[int64][]AlertDelivery, error)

AlertDeliveriesFor returns the delivery attempts for the given events.

func (*Store) AlertEventHost added in v1.6.0

func (s *Store) AlertEventHost(ctx context.Context, id int64) (int64, error)

AlertEventHost returns the Docker host an alert event belongs to.

Exists so a caller holding only an alert id can authorise against that alert's host before reading anything about it. Alert ids are sequential integers, so "you need the id first" is not an access control.

func (*Store) Audit

func (s *Store) Audit(ctx context.Context, e AuditEntry) error

Audit appends an entry to the audit log. Failures are returned but callers generally log-and-continue: an audit write must never block a user action.

func (*Store) AuthByID

func (s *Store) AuthByID(ctx context.Context, id int64) (*RegistryAuth, error)

AuthByID returns the decrypted credentials for a single registry.

func (*Store) AuthForHost

func (s *Store) AuthForHost(ctx context.Context, host string) (*RegistryAuth, error)

AuthForHost returns the decrypted credentials whose address matches the registry host of an image reference, or ErrNotFound if none is configured.

func (*Store) BackupTo added in v1.6.0

func (s *Store) BackupTo(ctx context.Context, path string) error

BackupTo writes a consistent snapshot of the database to path using `VACUUM INTO`. The database runs in WAL mode, so copying the file directly is unsafe: committed data can still live in the -wal file, and a copy taken during a write yields a torn database. VACUUM INTO takes the snapshot through the live connection instead, so it is safe while the server is running.

func (*Store) BumpSessionEpoch added in v1.6.0

func (s *Store) BumpSessionEpoch(ctx context.Context, userID int64) error

BumpSessionEpoch invalidates every session token already issued for a user.

A JWT is self-contained: nothing about changing a password reaches the copy a browser (or a script) already holds, so without this an attacker whose access prompted the reset keeps it until the token expires — up to twelve hours of full Docker control, granted by the very act meant to revoke it.

func (*Store) BurnFactorCounter added in v1.6.0

func (s *Store) BurnFactorCounter(ctx context.Context, id, counter int64) error

BurnFactorCounter records the time step a code came from, and that the factor was used. The counter must move forward: replaying the same code inside its 30-second window has to fail, which is the whole point of storing it.

func (*Store) CanReachHost added in v1.6.0

func (s *Store) CanReachHost(ctx context.Context, u *User, hostID int64) (bool, error)

CanReachHost reports whether a user may see anything at all on hostID. The local daemon (0) is always reachable, matching Grant.HasHost.

func (*Store) Close

func (s *Store) Close() error

Close releases the underlying database handle.

func (*Store) ComposeFragmentByID added in v1.4.0

func (s *Store) ComposeFragmentByID(ctx context.Context, id int64) (*ComposeFragment, error)

func (*Store) ComposeFragmentBySlug added in v1.4.0

func (s *Store) ComposeFragmentBySlug(ctx context.Context, slug string) (*ComposeFragment, error)

func (*Store) ConsumeOAuthCode added in v1.4.0

func (s *Store) ConsumeOAuthCode(ctx context.Context, codeHash string) (*OAuthCode, error)

ConsumeOAuthCode atomically fetches and deletes an authorization code, so a code can never be redeemed twice. Returns ErrNotFound if absent. Callers must still check ExpiresAt.

func (*Store) ConsumeRefreshToken added in v1.4.0

func (s *Store) ConsumeRefreshToken(ctx context.Context, tokenHash string) (*OAuthRefreshToken, error)

ConsumeRefreshToken atomically fetches and deletes a refresh token (rotation: every use invalidates the old token and a fresh one is issued). Returns ErrNotFound if absent. Callers must still check ExpiresAt.

func (*Store) CountAdmins

func (s *Store) CountAdmins(ctx context.Context) (int, error)

CountAdmins returns how many admin accounts exist (to guard the last admin).

func (*Store) CountFactors added in v1.6.0

func (s *Store) CountFactors(ctx context.Context, userID int64) (int, error)

CountFactors reports how many factors an account has paired.

func (*Store) CountUnacknowledged

func (s *Store) CountUnacknowledged(ctx context.Context) (int, error)

CountUnacknowledged returns the number of unacknowledged alert events.

func (*Store) CountUsers

func (s *Store) CountUsers(ctx context.Context) (int, error)

CountUsers returns the number of accounts; used to detect first-run setup.

func (*Store) CreateAPIToken added in v1.4.0

func (s *Store) CreateAPIToken(ctx context.Context, t *APIToken) (int64, error)

CreateAPIToken inserts a new token row and returns its assigned ID. The caller is responsible for generating the secret and passing its SHA-256 hash.

func (*Store) CreateAlertRule

func (s *Store) CreateAlertRule(ctx context.Context, r *AlertRule) (int64, error)

CreateAlertRule inserts an alert rule and returns its ID.

func (*Store) CreateComposeFragment added in v1.4.0

func (s *Store) CreateComposeFragment(ctx context.Context, f *ComposeFragment) (int64, error)

func (*Store) CreateFactor added in v1.6.0

func (s *Store) CreateFactor(ctx context.Context, f *AuthFactor, authorised bool) (int64, error)

CreateFactor pairs a new factor and returns its id.

authorised says the password was proved. When it is false the caller is relying on "this account has nothing to protect yet", and THAT is checked here, as part of the INSERT — not by the caller beforehand.

The distinction matters because the gap between a caller's check and this write is attacker-controlled. The WebAuthn library reads the request body, so a client that sends its headers and then stalls holds the handler open between the two for as long as it likes: the check runs against an unprotected account, the owner pairs their real factor, and the insert lands afterwards. Making the condition part of the write closes that, and the plain race with it — the same reason PairPendingFactor claims its enrolment with a compare-and-swap instead of a read.

Creating a factor also drops any half-finished TOTP enrolment. That enrolment is a live capability authorised against an account with nothing to protect, and this is the moment the account stops being unprotected — leaving it behind would let whoever started it add their own authenticator afterwards, with no password. All of it goes together or not at all.

func (*Store) CreateFirstUser added in v1.6.0

func (s *Store) CreateFirstUser(ctx context.Context, u *User) (int64, error)

CreateFirstUser inserts the first account, and only the first.

Setup is otherwise a check-then-act: NeedsSetup counts, the handler validates, and the insert happens later — so two requests arriving together can both pass the count and both create an admin. The window is small but the payoff is permanent admin on a fresh instance, and a fresh instance is exactly what is reachable before anyone is watching. The condition therefore lives in the INSERT itself, where SQLite settles it: zero rows affected means somebody else was first.

func (*Store) CreateHost

func (s *Store) CreateHost(ctx context.Context, h *Host) (int64, error)

CreateHost inserts a new host and returns its ID. The TLS private key is encrypted at rest (CA and client cert are public, so they're stored as-is).

func (*Store) CreateOAuthClient added in v1.4.0

func (s *Store) CreateOAuthClient(ctx context.Context, c *OAuthClient) error

CreateOAuthClient stores a newly registered client.

func (*Store) CreateOAuthCode added in v1.4.0

func (s *Store) CreateOAuthCode(ctx context.Context, codeHash string, c *OAuthCode) error

CreateOAuthCode stores an authorization code (by hash).

func (*Store) CreateParseRule

func (s *Store) CreateParseRule(ctx context.Context, name, pattern string) (int64, error)

CreateParseRule inserts a parse rule and returns its ID.

func (*Store) CreateProject added in v1.2.0

func (s *Store) CreateProject(ctx context.Context, p *Project) (int64, error)

CreateProject inserts a project and returns its ID. A slug collision yields ErrDuplicate.

func (*Store) CreateProjectTemplate added in v1.4.0

func (s *Store) CreateProjectTemplate(ctx context.Context, t *ProjectTemplate) (int64, error)

func (*Store) CreateRefreshToken added in v1.4.0

func (s *Store) CreateRefreshToken(ctx context.Context, tokenHash string, t *OAuthRefreshToken) error

CreateRefreshToken stores a refresh token (by hash).

func (*Store) CreateRegistry

func (s *Store) CreateRegistry(ctx context.Context, name, address, username, secret string) (int64, error)

CreateRegistry stores a registry, encrypting the secret. The address is normalised so it matches image references later (see NormalizeRegistryHost).

func (*Store) CreateRole added in v1.6.0

func (s *Store) CreateRole(ctx context.Context, r *Role) (int64, error)

CreateRole inserts a user-defined role. A duplicate name yields ErrDuplicate.

func (*Store) CreateServiceBlock added in v1.4.0

func (s *Store) CreateServiceBlock(ctx context.Context, b *ServiceBlock) (int64, error)

func (*Store) CreateSession added in v1.6.0

func (s *Store) CreateSession(ctx context.Context, sess *Session) error

CreateSession records a new session at login.

func (*Store) CreateUser

func (s *Store) CreateUser(ctx context.Context, u *User) (int64, error)

CreateUser inserts a new account and returns its assigned ID.

func (*Store) CreateWebhook

func (s *Store) CreateWebhook(ctx context.Context, w *Webhook) (int64, error)

CreateWebhook inserts a webhook and returns its ID.

func (*Store) DeleteAlertRule

func (s *Store) DeleteAlertRule(ctx context.Context, id int64) error

DeleteAlertRule removes an alert rule by ID.

func (*Store) DeleteAlertState added in v1.6.0

func (s *Store) DeleteAlertState(ctx context.Context, hostID int64, containerID, metric string) error

DeleteAlertState clears a condition that no longer holds.

func (*Store) DeleteComposeFragment added in v1.4.0

func (s *Store) DeleteComposeFragment(ctx context.Context, id int64) error

func (*Store) DeleteExpiredOAuth added in v1.4.0

func (s *Store) DeleteExpiredOAuth(ctx context.Context) error

DeleteExpiredOAuth purges authorization codes and refresh tokens whose expiry has passed (issued-but-never-redeemed codes, lapsed refresh tokens). Run periodically so the tables don't grow unbounded.

func (*Store) DeleteFactor added in v1.6.0

func (s *Store) DeleteFactor(ctx context.Context, id, userID int64) error

DeleteFactor removes one factor, unless it is the last one.

Scoped by user id as well as factor id, so knowing another account's factor id achieves nothing — and the "is this the last one?" test is part of the DELETE rather than a read before it. Counting first and deleting second is a race two concurrent requests win together: both see two factors, both delete, and the account is left with none. That is not the self-lockout it looks like — 2FA is derived from whether any factor exists, so zero factors means the password alone signs in. The guard has to be atomic or it is decoration.

func (*Store) DeleteHost

func (s *Store) DeleteHost(ctx context.Context, id int64) error

DeleteHost removes a host by ID.

func (*Store) DeleteOAuthClient added in v1.4.0

func (s *Store) DeleteOAuthClient(ctx context.Context, id string) (bool, error)

DeleteOAuthClient removes a registered client and, in the same transaction, any authorization codes and refresh tokens issued to it — so de-registering a client immediately severs every credential derived from it. The bool reports whether a client row actually existed (false → unknown id → 404).

func (*Store) DeleteParseRule

func (s *Store) DeleteParseRule(ctx context.Context, id int64) error

DeleteParseRule removes a parse rule by ID.

func (*Store) DeleteProject added in v1.2.0

func (s *Store) DeleteProject(ctx context.Context, id int64) error

DeleteProject removes the project row (the caller removes the folder).

func (*Store) DeleteProjectTemplate added in v1.4.0

func (s *Store) DeleteProjectTemplate(ctx context.Context, id int64) error

func (*Store) DeleteRegistry

func (s *Store) DeleteRegistry(ctx context.Context, id int64) error

DeleteRegistry removes a registry by ID.

func (*Store) DeleteRole added in v1.6.0

func (s *Store) DeleteRole(ctx context.Context, id int64) error

DeleteRole removes a user-defined role and any assignments of it. Built-ins are refused.

func (*Store) DeleteServiceBlock added in v1.4.0

func (s *Store) DeleteServiceBlock(ctx context.Context, id int64) error

func (*Store) DeleteSession added in v1.6.0

func (s *Store) DeleteSession(ctx context.Context, id string, userID int64) error

DeleteSession revokes one session. Scoped by user id as well as session id, so knowing (or guessing) another account's session id achieves nothing.

func (*Store) DeleteUser

func (s *Store) DeleteUser(ctx context.Context, id int64) error

DeleteUser removes an account.

func (*Store) DeleteUserFactors added in v1.6.0

func (s *Store) DeleteUserFactors(ctx context.Context, userID int64) error

DeleteUserFactors removes every factor for a user, for account deletion.

func (*Store) DeleteUserSessions added in v1.6.0

func (s *Store) DeleteUserSessions(ctx context.Context, userID int64) error

DeleteUserSessions revokes every session for a user — sign out everywhere, and what a password change does.

func (*Store) DeleteWebhook

func (s *Store) DeleteWebhook(ctx context.Context, id int64) error

DeleteWebhook removes a webhook by ID.

func (*Store) DisabledSections

func (s *Store) DisabledSections(ctx context.Context) ([]string, error)

DisabledSections returns the sections an admin has turned off app-wide.

func (*Store) EffectiveGrants added in v1.6.0

func (s *Store) EffectiveGrants(ctx context.Context, u *User) (map[string]Grant, error)

EffectiveGrants computes a user's access per section: the union of their roles' grants and their own per-user section list, then capped.

  • A per-user section (the pre-roles model) grants write unless the account is read-only, which is exactly how it behaved before roles existed.
  • A role section grants write only if the role says so.
  • The user-level read-only flag caps everything to reads, so it keeps meaning "this account cannot change anything" regardless of role.
  • App-wide disabled sections are removed last: a feature turned off is off for everyone (admins bypass this elsewhere, in checkAccess).

Admins are not special-cased here; checkAccess short-circuits for them, and keeping this function purely about grants makes it testable on its own.

func (*Store) EffectiveSections added in v1.6.0

func (s *Store) EffectiveSections(ctx context.Context, u *User) ([]string, error)

EffectiveSections lists the sections a user can reach, sorted — used for the users list and for LDAP section syncing.

func (*Store) EncryptPlaintextHostKeys added in v1.5.0

func (s *Store) EncryptPlaintextHostKeys(ctx context.Context) error

EncryptPlaintextHostKeys re-encrypts any host TLS private key still stored in plaintext (rows created before encryption-at-rest). Called once at startup, after the cipher is set; a no-op when there's nothing to migrate.

func (*Store) EnsureLocalHost

func (s *Store) EnsureLocalHost(ctx context.Context) error

EnsureLocalHost guarantees a "local" host row exists so the app is usable immediately on first run without manual host configuration.

func (*Store) ExistingRoleIDs added in v1.6.0

func (s *Store) ExistingRoleIDs(ctx context.Context, ids []int64) ([]int64, error)

ExistingRoleIDs filters ids down to the ones that still name a role, keeping the caller's order. Used when applying LDAP mappings, where an id can outlive the role it referred to.

func (*Store) FactorByCredentialID added in v1.6.0

func (s *Store) FactorByCredentialID(ctx context.Context, credentialID string) (*AuthFactor, error)

FactorByCredentialID finds a passkey by the credential id an assertion names.

NOT scoped to a user, deliberately: an assertion arrives naming a credential and the account it belongs to is the answer, not the question. That is safe because the id is unique across the table (see the index) and because the caller still has to verify the signature against this credential's public key — knowing an id proves nothing on its own.

func (*Store) FactorByID added in v1.6.0

func (s *Store) FactorByID(ctx context.Context, id, userID int64) (*AuthFactor, error)

FactorByID returns one factor, scoped to its owner.

func (*Store) GetLDAP

func (s *Store) GetLDAP(ctx context.Context) (LDAPConfig, error)

GetLDAP loads the LDAP config, decrypting the bind password.

func (*Store) GetSMTP

func (s *Store) GetSMTP(ctx context.Context) (SMTPConfig, error)

GetSMTP loads the SMTP config, decrypting the password.

func (*Store) HostByID

func (s *Store) HostByID(ctx context.Context, id int64) (*Host, error)

HostByID returns a single host or ErrNotFound.

func (*Store) InsertAlertEvent

func (s *Store) InsertAlertEvent(ctx context.Context, e *AlertEvent) (int64, error)

InsertAlertEvent records an alert and returns its ID.

A resolution is stored ALREADY ACKNOWLEDGED. It is not a task — there is nothing for anyone to do about a condition that ended — so it must never sit in an outstanding-work list or push up the sidebar count. Settling it here, once, is why nothing downstream needs a special case for it; and if the UI ever offers an Acknowledge action on resolutions again, the data still means what it says. AcknowledgedBy stays empty, because no person did it.

func (*Store) ListAPITokens added in v1.4.0

func (s *Store) ListAPITokens(ctx context.Context, userID int64) ([]APIToken, error)

ListAPITokens returns a user's tokens (newest first) for the management UI. The hash is included but is not the secret — the secret is unrecoverable.

func (*Store) ListAlertEvents

func (s *Store) ListAlertEvents(ctx context.Context, q AlertQuery) ([]AlertEvent, int, error)

ListAlertEvents returns a page of the event feed, newest first, plus the total number of events matching the filter so a caller can page through it.

The filter is built as parameterised fragments rather than string-concatenated values: every one of these comes from a query string.

func (*Store) ListAlertRules

func (s *Store) ListAlertRules(ctx context.Context) ([]AlertRule, error)

ListAlertRules returns all alert rules.

func (*Store) ListAlertStates added in v1.6.0

func (s *Store) ListAlertStates(ctx context.Context) ([]AlertState, error)

ListAlertStates returns every condition currently held to be firing.

func (*Store) ListAllAPITokens added in v1.4.0

func (s *Store) ListAllAPITokens(ctx context.Context) ([]APITokenWithUser, error)

ListAllAPITokens returns every user's tokens (newest first), each annotated with the owner's username, for the admin overview. Revoked tokens are included so an admin can see recently-revoked credentials; the handler/UI distinguishes them via the Revoked flag. The token hash is deliberately NOT selected — the overview is metadata-only, so the digest never even reaches process memory here (no chance of leaking via a log line or panic).

func (*Store) ListComposeFragments added in v1.4.0

func (s *Store) ListComposeFragments(ctx context.Context) ([]ComposeFragment, error)

func (*Store) ListFactors added in v1.6.0

func (s *Store) ListFactors(ctx context.Context, userID int64) ([]AuthFactor, error)

ListFactors returns a user's paired factors, oldest first — the order they were added is the order that makes sense of "which one is my old phone".

func (*Store) ListHosts

func (s *Store) ListHosts(ctx context.Context) ([]Host, error)

ListHosts returns all configured hosts ordered by name.

func (*Store) ListOAuthClients added in v1.4.0

func (s *Store) ListOAuthClients(ctx context.Context) ([]OAuthClient, error)

ListOAuthClients returns every registered MCP OAuth client (newest first) for the admin overview. Clients are public (no secret stored), so the full row is safe to surface.

func (*Store) ListParseRules

func (s *Store) ListParseRules(ctx context.Context) ([]ParseRule, error)

ListParseRules returns all saved log-parsing rules.

func (*Store) ListProjectTemplates added in v1.4.0

func (s *Store) ListProjectTemplates(ctx context.Context) ([]ProjectTemplate, error)

func (*Store) ListProjects added in v1.2.0

func (s *Store) ListProjects(ctx context.Context) ([]Project, error)

ListProjects returns all projects ordered by name.

func (*Store) ListRegistries

func (s *Store) ListRegistries(ctx context.Context) ([]Registry, error)

ListRegistries returns the configured registries without their secrets.

func (*Store) ListRoles added in v1.6.0

func (s *Store) ListRoles(ctx context.Context) ([]Role, error)

ListRoles returns every role with its grants, ordered built-ins first then by name (mirroring how Templates lists built-in presets ahead of user ones).

func (*Store) ListServiceBlocks added in v1.4.0

func (s *Store) ListServiceBlocks(ctx context.Context) ([]ServiceBlock, error)

func (*Store) ListSessions added in v1.6.0

func (s *Store) ListSessions(ctx context.Context, userID int64) ([]Session, error)

ListSessions returns a user's own sessions, newest first, dropping any that have expired (a token past its expiry is refused anyway, so listing it would only invite people to revoke something already gone).

func (*Store) ListUsers

func (s *Store) ListUsers(ctx context.Context) ([]User, error)

ListUsers returns all accounts (without secrets) for the admin user manager.

func (*Store) ListWebhooks

func (s *Store) ListWebhooks(ctx context.Context) ([]Webhook, error)

ListWebhooks returns all configured webhooks.

func (*Store) LocalhostNo2FA

func (s *Store) LocalhostNo2FA(ctx context.Context) (bool, error)

LocalhostNo2FA reports whether password-only login is allowed from loopback.

func (*Store) MCPTokenPolicy added in v1.6.0

func (s *Store) MCPTokenPolicy(ctx context.Context) (MCPTokenPolicy, error)

MCPTokenPolicy reads the policy, falling back to the default when unset.

func (*Store) NormalizeHostID added in v1.6.0

func (s *Store) NormalizeHostID(ctx context.Context, id int64) int64

NormalizeHostID collapses the two ways the local daemon can be named into one. A request may say host 0 ("the default local host") or the id of the seeded `kind = 'local'` row — they mean the same daemon, and host scoping has to treat them the same or the local daemon ends up reachable under one name and not the other. Returns 0 for either, and id unchanged for a remote host.

func (*Store) OAuthClientByID added in v1.4.0

func (s *Store) OAuthClientByID(ctx context.Context, id string) (*OAuthClient, error)

OAuthClientByID looks up a registered client.

func (*Store) PairPendingFactor added in v1.6.0

func (s *Store) PairPendingFactor(ctx context.Context, userID int64, pending, name string) (int64, error)

PairPendingFactor turns the account's pending enrolment into a factor, atomically.

The caller has already checked a code against `pending`. Between that check and this call, anything could have happened — including the same request arriving sixteen times in parallel, which is a POST away. So the claim on the pending secret is a compare-and-swap: exactly one caller clears it, and only that caller inserts. Without it, one enrolment becomes N factors holding ONE secret, and since the replay watermark is per factor, every future code from that authenticator becomes spendable N times.

func (*Store) Ping added in v1.1.0

func (s *Store) Ping(ctx context.Context) error

Ping checks that the database is reachable (used by the health endpoint).

func (*Store) ProjectByID added in v1.2.0

func (s *Store) ProjectByID(ctx context.Context, id int64) (*Project, error)

ProjectByID looks up a project by primary key.

func (*Store) ProjectTemplateByID added in v1.4.0

func (s *Store) ProjectTemplateByID(ctx context.Context, id int64) (*ProjectTemplate, error)

func (*Store) ProjectTemplateBySlug added in v1.4.0

func (s *Store) ProjectTemplateBySlug(ctx context.Context, slug string) (*ProjectTemplate, error)

func (*Store) PromoteTOTPPending added in v1.6.0

func (s *Store) PromoteTOTPPending(ctx context.Context, userID int64) error

PromoteTOTPPending makes the pending secret the active one and clears it. Used once the user has proved they can generate codes from the new authenticator.

func (*Store) PurgeExpiredSessions added in v1.6.0

func (s *Store) PurgeExpiredSessions(ctx context.Context) error

PurgeExpiredSessions drops rows whose tokens can no longer be presented.

func (*Store) ReachableHosts added in v1.6.0

func (s *Store) ReachableHosts(ctx context.Context, u *User) (hosts map[int64]bool, all bool, err error)

ReachableHosts is the union of every host a user's grants reach, across all sections. all=true means "no restriction anywhere" — either an unscoped grant exists or the user is unconstrained, and the host set is then irrelevant.

It answers a coarser question than EffectiveGrants: not "may they do X on host N" but "may they see host N at all". That is what the aggregate views need — the host list, the project list, the alert feed — and what the handful of routes that take ?host= without belonging to a section need, since there is no single section to check them against.

func (*Store) RecentAudit

func (s *Store) RecentAudit(ctx context.Context, limit int, before int64) ([]AuditEntry, error)

RecentAudit returns the most recent audit entries, newest first. When before is > 0, only entries older than that id are returned (cursor pagination).

func (*Store) RecordAlertDelivery added in v1.6.0

func (s *Store) RecordAlertDelivery(ctx context.Context, d *AlertDelivery) error

RecordAlertDelivery stores the outcome of one delivery attempt.

func (*Store) RevokeAPIToken added in v1.4.0

func (s *Store) RevokeAPIToken(ctx context.Context, id, userID int64) (bool, error)

RevokeAPIToken marks a token revoked. It is scoped to userID so a caller can only revoke their own tokens. The bool reports whether a matching, owned token was actually revoked (false → unknown id or not the caller's), so the handler can return 404 instead of a misleading success.

func (*Store) RoleByID added in v1.6.0

func (s *Store) RoleByID(ctx context.Context, id int64) (*Role, error)

RoleByID looks up one role with its grants.

func (*Store) RoleIDsForUser added in v1.6.0

func (s *Store) RoleIDsForUser(ctx context.Context, userID int64) ([]int64, error)

RoleIDsForUser returns the ids of the roles assigned to a user.

func (*Store) RolesForUser added in v1.6.0

func (s *Store) RolesForUser(ctx context.Context, userID int64) ([]Role, error)

RolesForUser returns the roles assigned to a user, with their grants.

func (*Store) ServiceBlockByID added in v1.4.0

func (s *Store) ServiceBlockByID(ctx context.Context, id int64) (*ServiceBlock, error)

func (*Store) ServiceBlockBySlug added in v1.4.0

func (s *Store) ServiceBlockBySlug(ctx context.Context, slug string) (*ServiceBlock, error)

func (*Store) SessionEpoch added in v1.6.0

func (s *Store) SessionEpoch(ctx context.Context, userID int64) (int64, error)

SessionEpoch returns the account's current session generation. A token minted before the last bump is stale and must be refused.

func (*Store) SessionExists added in v1.6.0

func (s *Store) SessionExists(ctx context.Context, id string, userID int64) (bool, error)

SessionExists reports whether a session id is still valid for that user.

func (*Store) SetAlertRuleEnabled

func (s *Store) SetAlertRuleEnabled(ctx context.Context, id int64, enabled bool) error

SetAlertRuleEnabled toggles an alert rule on or off.

func (*Store) SetAuthSource added in v1.6.0

func (s *Store) SetAuthSource(ctx context.Context, userID int64, source string) error

SetAuthSource records which authority owns this account's password. Used by the LDAP provisioning path, and by tests that need an account this app does not own.

func (*Store) SetCipher

func (s *Store) SetCipher(c *crypto.Cipher)

SetCipher installs the cipher used to encrypt secrets at rest (registry credentials). It is wired up once at startup, after the key is loaded.

func (*Store) SetDisabledSections

func (s *Store) SetDisabledSections(ctx context.Context, keys []string) error

SetDisabledSections persists the app-wide disabled sections.

func (*Store) SetHostAlertEmail

func (s *Store) SetHostAlertEmail(ctx context.Context, id int64, email string) error

SetHostAlertEmail sets a host's per-host alert recipient override.

func (*Store) SetHostDisabled added in v1.2.0

func (s *Store) SetHostDisabled(ctx context.Context, id int64, disabled bool) error

SetHostDisabled toggles whether the monitor ignores a host.

func (*Store) SetHostKey

func (s *Store) SetHostKey(ctx context.Context, id int64, key string) error

SetHostKey pins (or clears, when key is "") the trusted SSH host public key for a host. Subsequent connections verify the daemon's key against it.

func (*Store) SetLDAP

func (s *Store) SetLDAP(ctx context.Context, c LDAPConfig) error

SetLDAP persists the config, encrypting the bind password. An empty bind password preserves the previously stored one.

func (*Store) SetLastDeployedProfiles added in v1.6.1

func (s *Store) SetLastDeployedProfiles(ctx context.Context, id int64, profiles []string) error

SetLastDeployedProfiles records the profiles used on a project's last successful `compose up`, so the UI can tell "currently deployed" apart from whatever's merely selected for the next deploy. Called only after a deploy actually succeeds — a failed deploy must not overwrite what's still running.

func (*Store) SetLocalhostNo2FA

func (s *Store) SetLocalhostNo2FA(ctx context.Context, on bool) error

SetLocalhostNo2FA toggles the localhost 2FA exemption.

func (*Store) SetMCPTokenPolicy added in v1.6.0

func (s *Store) SetMCPTokenPolicy(ctx context.Context, p MCPTokenPolicy) error

SetMCPTokenPolicy persists the policy after normalising it.

func (*Store) SetPasswordless added in v1.6.0

func (s *Store) SetPasswordless(ctx context.Context, userID int64, on bool) error

SetPasswordless turns signing in with a passkey alone on or off for one account.

func (*Store) SetSMTP

func (s *Store) SetSMTP(ctx context.Context, c SMTPConfig) error

SetSMTP persists the SMTP config, encrypting the password. An empty password preserves the previously stored one (so the UI need not resend the secret).

func (*Store) SetSetting

func (s *Store) SetSetting(ctx context.Context, key, value string) error

SetSetting upserts a key/value pair.

func (*Store) SetTOTPLastCounter added in v1.6.0

func (s *Store) SetTOTPLastCounter(ctx context.Context, userID, counter int64) error

SetTOTPLastCounter records the time step whose code was just accepted, so the same code cannot be presented again while it is still inside its window.

Written with a `>` guard rather than a plain assignment: two requests carrying the same code can race here, and the loser must not be able to move the watermark backwards.

func (*Store) SetTOTPPending added in v1.6.0

func (s *Store) SetTOTPPending(ctx context.Context, userID int64, secret string, stepUp bool) error

SetTOTPPending stores a secret for an authenticator being paired while another one is still active, without touching the working secret.

stepUp records whether the password was proved to start this enrolment, so that redeeming it can be judged against the account's protection at that time rather than at the time the button was pressed.

func (*Store) SetUserEmail added in v1.6.0

func (s *Store) SetUserEmail(ctx context.Context, id int64, email string) error

SetUserEmail records where a user's own alert e-mails go. It is self-service — an account edits its own address — and is also written by the LDAP sync when the directory publishes one.

func (*Store) SetUserPrefs added in v1.1.0

func (s *Store) SetUserPrefs(ctx context.Context, userID int64, prefs string) error

SetUserPrefs replaces a user's UI preferences JSON blob.

func (*Store) SetUserRoles added in v1.6.0

func (s *Store) SetUserRoles(ctx context.Context, userID int64, roleIDs []int64) error

SetUserRoles replaces a user's role assignments. Unknown role ids are dropped rather than erroring, so a stale UI can't wedge the form.

func (*Store) Setting

func (s *Store) Setting(ctx context.Context, key string) (string, error)

Setting reads a single key from the settings table. Returns ("", nil) when the key is absent so callers can treat "missing" as "use default".

func (*Store) TouchAPIToken added in v1.4.0

func (s *Store) TouchAPIToken(ctx context.Context, id int64) error

TouchAPIToken records the last time a token was used. Best-effort: callers ignore the error so a logging write never blocks an authenticated request.

func (*Store) TouchLogin

func (s *Store) TouchLogin(ctx context.Context, userID int64) error

TouchLogin records the timestamp of a successful login.

func (*Store) TouchProject added in v1.2.0

func (s *Store) TouchProject(ctx context.Context, id int64) error

TouchProject bumps updated_at (called when a file changes).

func (*Store) TouchSession added in v1.6.0

func (s *Store) TouchSession(ctx context.Context, id string) error

TouchSession records that a session was used, at minute granularity.

Written only when the stored value is already a minute old: this runs on every authenticated request, and a write per request would turn a read-mostly workload into a write-mostly one against a single-writer database.

func (*Store) UpdateAlertRule

func (s *Store) UpdateAlertRule(ctx context.Context, id int64, r *AlertRule) error

UpdateAlertRule replaces a rule's mutable fields (enabled is managed separately via SetAlertRuleEnabled).

func (*Store) UpdateComposeFragment added in v1.4.0

func (s *Store) UpdateComposeFragment(ctx context.Context, f *ComposeFragment) error

UpdateComposeFragment edits a fragment's editable fields (the slug is immutable).

func (*Store) UpdateCredential added in v1.6.0

func (s *Store) UpdateCredential(ctx context.Context, id int64, credential string) error

UpdateCredential writes back the credential after a successful assertion — the signature counter moves, and that movement is what detects a cloned key.

func (*Store) UpdatePassword

func (s *Store) UpdatePassword(ctx context.Context, userID int64, hash string) error

UpdatePassword replaces the stored Argon2id hash for a user.

func (*Store) UpdateProjectSettings added in v1.6.0

func (s *Store) UpdateProjectSettings(ctx context.Context, id int64, name string, hostID int64, allowRemoteHostPaths bool) error

UpdateProjectSettings changes the display name, target host and the remote-host-path opt-in (the slug stays immutable).

func (*Store) UpdateProjectTemplate added in v1.4.0

func (s *Store) UpdateProjectTemplate(ctx context.Context, id int64, name, description string) error

UpdateProjectTemplate changes a template's display name and description. The slug (its stable identifier on disk and in create references) is immutable, so renames never move files — mirrors how project renames work.

func (*Store) UpdateRole added in v1.6.0

func (s *Store) UpdateRole(ctx context.Context, id int64, name, description string, sections []RoleSection, hostIDs []int64) error

UpdateRole renames a role and replaces its grants. Built-in roles are refused: they are the known-good baseline, and the UI offers Duplicate instead.

func (*Store) UpdateServiceBlock added in v1.4.0

func (s *Store) UpdateServiceBlock(ctx context.Context, b *ServiceBlock) error

UpdateServiceBlock changes a block's editable fields. The slug stays immutable (it backs the builder reference), like project/template renames.

func (*Store) UpdateUserAccess

func (s *Store) UpdateUserAccess(ctx context.Context, id int64, role string, readOnly bool, sections []string) error

UpdateUserAccess changes a user's role, read-only flag and allowed sections.

func (*Store) UpsertAlertState added in v1.6.0

func (s *Store) UpsertAlertState(ctx context.Context, a *AlertState) error

UpsertAlertState records or updates a firing condition. StartedAt is written only on insert, so the age of an incident survives escalation and re-notify.

func (*Store) UserByID

func (s *Store) UserByID(ctx context.Context, id int64) (*User, error)

UserByID looks up a user by primary key.

func (*Store) UserByUsername

func (s *Store) UserByUsername(ctx context.Context, username string) (*User, error)

UserByUsername looks up a user by their unique username.

func (*Store) UserByWebAuthnHandle added in v1.6.0

func (s *Store) UserByWebAuthnHandle(ctx context.Context, handle []byte) (*User, error)

UserByWebAuthnHandle resolves the account a user handle belongs to.

func (*Store) UserPrefs added in v1.1.0

func (s *Store) UserPrefs(ctx context.Context, userID int64) (string, error)

UserPrefs returns a user's UI preferences as a JSON object string ("{}" if none). These are opaque to the server — the frontend owns the shape.

func (*Store) WebAuthnHandle added in v1.6.0

func (s *Store) WebAuthnHandle(ctx context.Context, userID int64) ([]byte, error)

WebAuthnHandle returns the account's opaque user handle, creating one on first use. It is what an authenticator stores next to the key, so it has to be stable for the life of the account and must not be derived from anything guessable or meaningful — the spec asks for 64 random bytes, and a username would leak into the authenticator's own storage.

func (*Store) WebhookByID

func (s *Store) WebhookByID(ctx context.Context, id int64) (*Webhook, error)

WebhookByID returns one webhook by ID (ErrNotFound if missing).

type TokenLifetimeError added in v1.6.0

type TokenLifetimeError struct{ MaxDays int }

TokenLifetimeError reports a requested lifetime beyond the policy ceiling. A typed error so the message can name the actual limit — a bare "too long" leaves the user guessing what to type instead.

func (*TokenLifetimeError) Error added in v1.6.0

func (e *TokenLifetimeError) Error() string

type User

type User struct {
	ID           int64
	Username     string
	PasswordHash string
	Role         string
	// Email receives alerts from rules this user creates. Optional; when an LDAP
	// directory publishes a mail attribute it is synced here on login.
	Email      string
	AuthSource string // "local" (password stored here) or "ldap" (verified externally)
	ReadOnly   bool
	Sections   []string
	TOTPSecret string
	// TOTPEnabled means "has an authenticator app". MFAEnabled means "has a second
	// factor of any kind" — the two stopped being the same thing when passkeys
	// arrived, and the login path needs the second: an account with only a passkey
	// must still be challenged, and must not be asked for a code it cannot produce.
	TOTPEnabled bool
	MFAEnabled  bool
	// TOTPPending holds a secret being paired. It becomes a factor on confirmation,
	// and otherwise never takes effect: a wrong code, a cancel or a closed tab leave
	// it sitting here until something clears it. Do not treat its presence as "a
	// pairing is in progress".
	//
	// A stale value is NOT harmless, which is why it is cleared whenever a factor is
	// created and why TOTPPendingStepUp exists. It is a capability: an enrolment
	// begun on an unprotected account needs no password, so one left lying around
	// could otherwise be redeemed later, against an account that has since been
	// protected, by whoever started it.
	TOTPPending string
	// TOTPPendingStepUp records whether the password was proved when the pending
	// enrolment was started. Redeeming it on an account that has a second factor
	// requires this to be true — the check belongs at the moment the factor is
	// created, not only at the moment the enrolment begins.
	TOTPPendingStepUp bool
	// Passwordless says a passkey alone may sign this account in. Off unless the
	// owner turned it on with their password: it changes what the account rests on,
	// and for a synced passkey it moves that to the platform account.
	Passwordless bool
	// TOTPLastCounter is the last 30-second time step whose code was accepted.
	// A code is only valid once: within its window it would otherwise work
	// repeatedly, so one shoulder-surfed or phished code could be spent several
	// times — and the challenge token it satisfies lives for five minutes.
	TOTPLastCounter int64
	// SessionEpoch is bumped when previously issued sessions must stop working.
	SessionEpoch int64
	CreatedAt    time.Time
	LastLoginAt  time.Time
}

User is an application account. PasswordHash is an Argon2id encoded hash. TOTPSecret is the base32 shared secret; it is only meaningful once TOTPEnabled is true (i.e. the user confirmed enrollment with a valid code).

Role is "admin" (full access incl. user/feature management) or "user". For "user" accounts, Sections lists the menu sections they may access and ReadOnly blocks mutating actions. Admins ignore both.

func (*User) IsAdmin

func (u *User) IsAdmin() bool

IsAdmin reports whether the user has the admin role.

type Webhook

type Webhook struct {
	ID           int64             `json:"id"`
	Name         string            `json:"name"`
	URL          string            `json:"url"`
	Method       string            `json:"method"`
	Headers      map[string]string `json:"headers"`
	BodyTemplate string            `json:"bodyTemplate"`
	CreatedAt    time.Time         `json:"createdAt"`
}

Webhook is a generic HTTP destination an alert rule can fire to. body_template is a Go text/template rendered against the alert event.

Jump to

Keyboard shortcuts

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