store

package
v0.8.9 Latest Latest
Warning

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

Go to latest
Published: Jun 23, 2026 License: GPL-3.0 Imports: 14 Imported by: 0

Documentation

Overview

Package store owns ALL SQL in the gateway: users, groups, services, trusted networks, runtime system parameters, MFA state, and the audit trail, in one SQLite database (modernc.org/sqlite — pure Go, no cgo). No other package writes SQL; callers depend on narrow interface slices of *Store (auth.UserStore, server.AdminStore, …).

Two cross-cutting contracts live at this layer:

  • Canonical UPPERCASE names. Usernames, group names, and service NAMEs fold to upper case here — the single choke point — and the UNIQUE columns are COLLATE NOCASE, so Go-side case-sensitive compares (e.g. slices.Contains(groups, store.AdminGroup)) are correct as written. Passwords and service hosts are never normalized.

  • Versioned, forward-only schema migration (migrate.go). Structural changes append to the migrations ledger (never edit shipped steps); PRAGMA user_version stamps progress; Open refuses a newer-than-binary DB and writes a VACUUM INTO backup before migrating. Code-defined default rows (the ZZADMIN group, sysconfig.Catalog seeds) are applied by reconcileDefaults on every Open, outside the ledger, so new defaults reach already-migrated databases.

The store is mechanical by design: policy (admin guardrails, reserved group rules, duplicate-name messages) lives in internal/server's adminFlow.

Index

Constants

View Source
const (
	AdminGroup          = "ZZADMIN"
	ReservedGroupPrefix = "ZZ"
)

Reserved group namespace. Group names with the (case-insensitive) prefix ReservedGroupPrefix are dictated by the app: the admin UI can manage their membership but can never create or delete them. AdminGroup is the first such group; membership in it gates the 3270 admin screens. It is auto-created by migrate() so it always exists.

View Source
const (
	AuditConnect           = "connect"            // TCP session began
	AuditAuthOK            = "auth_ok"            // login success
	AuditAuthFail          = "auth_fail"          // login failure (attempted username, never the password)
	AuditAuthError         = "auth_error"         // infrastructure error during authentication (detail = error text)
	AuditBridgeStart       = "bridge_start"       // service selected, backend dial begins
	AuditBridgeEnd         = "bridge_end"         // bridge returned (detail = cause)
	AuditAdmin             = "admin"              // admin CRUD mutation (detail = change description)
	AuditLogout            = "logout"             // session ended login (detail: "user logoff" | "idle logout")
	AuditDisconnect        = "disconnect"         // connection ended (detail = how)
	AuditMFAEnrolled       = "mfa_enrolled"       // user completed self-enrollment
	AuditMFASuccess        = "mfa_success"        // correct code at login
	AuditMFAFailed         = "mfa_failed"         // incorrect code (enroll confirm or login)
	AuditMFACleared        = "mfa_cleared"        // admin wiped the secret
	AuditMFAEnforced       = "mfa_enforced"       // admin turned mfa_required on
	AuditPasswordSelf      = "password_self"      // user changed their own password (self-service)
	AuditSettingsLocked    = "settings_locked"    // admin locked a user out of self-service
	AuditSettingsUnlocked  = "settings_unlocked"  // admin restored a user's self-service
	AuditSessionDisconnect = "session_disconnect" // admin disconnected a live session (GH #91)
	AuditDocUpdate         = "doc_update"         // admin saved a document from the editor (detail = name + line count)
	AuditDocImport         = "doc_import"         // document replaced from a server file (detail = name + path + line count)
)

Audit event kinds. One session_id ties a connection's events together.

View Source
const (
	DocMOTD     = "MOTD"
	DocBranding = "BRANDING"
	// DocHelpMenu is the service-menu help text shown by PF1. The HELP-<panel>
	// prefix is the naming convention for future per-panel help documents.
	DocHelpMenu = "HELP-MENU"
)

Known document names. The set is code-defined: reconcileDefaults seeds one row per name so the admin member list always shows them all.

View Source
const (
	MaxServiceNameLen = 8
	MaxDescriptionLen = 40
)
View Source
const DefaultHelpMenuContent = `` /* 795-byte string literal not displayed */

DefaultHelpMenuContent is the stock HELP-MENU text seeded when the row is first created (see reconcileDefaults). The menu conventions it documents are app-defined, not site-specific, so every install gets working PF1 help out of the box; admins may edit or blank it and the edit sticks. Lines stay within the editor's 76 editable columns.

View Source
const MaxCommentLen = 40

MaxCommentLen is the byte cap on a trusted network's operator annotation (matches the 40-char admin form field).

View Source
const MaxDocumentBytes = 8 << 10 // 8 KiB

MaxDocumentBytes caps a document's content (matches the old file read cap).

Variables

View Source
var ErrDocumentTooLarge = errors.New("document too large (max 8 KiB)")

ErrDocumentTooLarge is returned when content (or an imported file) exceeds MaxDocumentBytes.

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

ErrNotFound is returned when a requested row does not exist.

View Source
var ErrSchemaNewer = errors.New("store: database schema is newer than this build")

ErrSchemaNewer is returned by Open when the database's schema version is higher than this build knows how to handle (a newer binary wrote it).

View Source
var KnownDocuments = []string{DocMOTD, DocBranding, DocHelpMenu}

KnownDocuments lists every valid document name.

Functions

func NormalizeDocName added in v0.8.1

func NormalizeDocName(name string) (string, error)

NormalizeDocName folds name to canonical uppercase and rejects names outside KnownDocuments (the single choke point, mirroring usernames/service names).

func NormalizeEmail

func NormalizeEmail(email string) string

NormalizeEmail trims surrounding space and lower-cases the address, so the column is a clean key for future email lookups. Empty in, empty out.

func NormalizeServiceName

func NormalizeServiceName(name string) (string, error)

NormalizeServiceName folds name to uppercase and validates it as a service identifier: 1-8 characters, A-Z and 0-9 only. It returns the normalized name or an error naming the rule violated.

func ParseTrustedCIDR

func ParseTrustedCIDR(s string) (netip.Prefix, error)

ParseTrustedCIDR validates and normalizes a trusted network string. A bare IP is expanded to a host route (/32 for IPv4, /128 for IPv6). The returned prefix is in canonical masked form (host bits zeroed).

func ReadDocumentFile added in v0.8.1

func ReadDocumentFile(path string) (string, error)

ReadDocumentFile reads a server-side file for import: absolute path required, rejected (not truncated) over MaxDocumentBytes. Used by the admin import screen and the CLI; the v3 migration has its own truncating reader to match the legacy render behavior.

func ValidateComment

func ValidateComment(comment string) error

ValidateComment enforces a required, length-bounded operator annotation.

func ValidateDescription

func ValidateDescription(desc string) error

ValidateDescription enforces a required, length-bounded service label.

func ValidateEmail

func ValidateEmail(email string) error

ValidateEmail does a deliberately permissive "looks like an address" check: empty is allowed (the field is optional); otherwise exactly one "@", a non-empty local part, a domain that is non-empty and contains a ".", and no spaces. This is intentionally loose and is expected to loosen further later for legacy / pre-SMTP address forms — keep the rule in this one function.

func ValidateFullName

func ValidateFullName(name string) error

ValidateFullName checks the optional display name: empty is allowed; a non-empty value reuses the description length cap (MaxDescriptionLen bytes).

Types

type AuditEvent

type AuditEvent struct {
	ID         int64
	At         time.Time
	SessionID  string
	Kind       string
	Username   string // the subject — the account this row is about ("" if none)
	Actor      string // the authenticated principal who performed the action ("" pre-auth)
	RemoteAddr string
	Service    string
	Detail     string
}

AuditEvent is one audit-trail row. Username is a plain string, not a user FK: rows must survive user deletion, and auth_fail records usernames that may not exist.

type AuditFilter

type AuditFilter struct {
	Username string // subject lens
	Actor    string // actor lens
	Kind     string
	Since    time.Time
	Limit    int
}

AuditFilter narrows ListAudit. Zero values mean "no constraint"; Limit <= 0 means the default of 100 rows.

type Document added in v0.8.1

type Document struct {
	Name      string
	Content   string // LF-joined lines; "" = empty (feature disabled)
	UpdatedAt string // UTC RFC3339; "" until first save
	UpdatedBy string // actor username; "" until first save
}

Document is one DB-resident text document.

func (Document) LineCount added in v0.8.1

func (d Document) LineCount() int

LineCount is the member-list Lines column.

func (Document) Lines added in v0.8.1

func (d Document) Lines() []string

Lines splits Content for the editor; nil for an empty document. A single trailing EOF newline does not count as an extra line; round-trip via the editor re-joins without it — the render path drops trailing blanks either way.

type Group

type Group struct {
	ID   int64
	Name string
}

Group is a named collection of users granted access to services.

type Service

type Service struct {
	ID          int64
	Name        string
	Description string
	Host        string
	Port        int
	TLS         bool
	TLSVerify   bool
}

Service is a backend TN3270 host the menu can offer.

type Store

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

Store wraps the SQLite database connection.

func Open

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

Open opens (creating if necessary) the SQLite database at path and applies the schema migration. The schema is idempotent.

func (*Store) AddUserToGroup

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

AddUserToGroup links a user to a group (idempotent).

func (*Store) Backup

func (s *Store) Backup(ctx context.Context, dest string) error

Backup writes a consistent single-file snapshot of the database to dest using VACUUM INTO (safe under WAL; produces no .wal/.shm side files). dest must not already exist. Callable any time, including while serving. This is also the primitive the future admin "Backup now" action will call.

func (*Store) ClearMFA

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

ClearMFA wipes the secret material (lost-device recovery), preserving the mfa_required flag so the user re-enrolls on next login if still required.

func (*Store) Close

func (s *Store) Close() error

Close closes the underlying database.

func (*Store) CountAdminMembers

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

CountAdminMembers returns the number of users in the AdminGroup (ZZADMIN).

func (*Store) CountEnrolledUsers

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

CountEnrolledUsers returns how many users have a stored secret (used by the startup fail-closed check).

func (*Store) CountGroupMembers

func (s *Store) CountGroupMembers(ctx context.Context, groupID int64) (int, error)

CountGroupMembers returns how many users belong to the group.

func (*Store) CountGroupServices

func (s *Store) CountGroupServices(ctx context.Context, groupID int64) (int, error)

CountGroupServices returns how many services the group can access.

func (*Store) CreateGroup

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

CreateGroup inserts a group, or returns the existing group's id.

func (*Store) CreateService

func (s *Store) CreateService(ctx context.Context, name, description, host string, port int, tls, verify bool) (int64, error)

CreateService inserts a service, or returns the existing service's id.

func (*Store) CreateTrustedNetwork

func (s *Store) CreateTrustedNetwork(ctx context.Context, cidr, comment string) (int64, error)

CreateTrustedNetwork inserts a trusted network, or returns the existing record's id if the CIDR already exists (idempotent, mirrors CreateService).

func (*Store) CreateUser

func (s *Store) CreateUser(ctx context.Context, username, passwordHash string) (int64, error)

CreateUser inserts a user, or returns the existing user's id if the username already exists (idempotent for seeding).

func (*Store) DeleteGroup

func (s *Store) DeleteGroup(ctx context.Context, groupID int64) error

DeleteGroup removes the group, its memberships, and its service links in one transaction. Callers enforce the ZZ* reservation; the store stays mechanical.

func (*Store) DeleteService

func (s *Store) DeleteService(ctx context.Context, serviceID int64) error

DeleteService removes the service and its group links in one transaction.

func (*Store) DeleteTrustedNetwork

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

DeleteTrustedNetwork removes a trusted network by id. Deleting an absent id is a silent no-op (the admin always deletes from a just-fetched list).

func (*Store) DeleteUser

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

DeleteUser removes the user and its group memberships in one transaction.

func (*Store) GetConfig

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

GetConfig returns the current value for key from system_config. Returns ErrNotFound if the key is not present (i.e. not in the catalog).

func (*Store) GetDocument added in v0.8.1

func (s *Store) GetDocument(ctx context.Context, name string) (Document, error)

GetDocument returns the named document, or ErrNotFound for a known name whose row is missing (cannot happen after reconcileDefaults; defensive).

func (*Store) GetMFASentinel

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

GetMFASentinel returns the stored key-check sentinel, or "" if none exists.

func (*Store) GetService

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

GetService returns the service by id, or ErrNotFound.

func (*Store) GetServiceByName

func (s *Store) GetServiceByName(ctx context.Context, name string) (Service, error)

GetServiceByName returns the service by name (case-insensitive), or ErrNotFound.

func (*Store) GetUserByUsername

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

GetUserByUsername returns the user, or ErrNotFound.

func (*Store) GetUserGroups

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

GetUserGroups returns the names of groups the user belongs to.

func (*Store) LinkGroupService

func (s *Store) LinkGroupService(ctx context.Context, groupID, serviceID int64) error

LinkGroupService grants a group access to a service (idempotent).

func (*Store) ListAllServices

func (s *Store) ListAllServices(ctx context.Context) ([]Service, error)

ListAllServices returns all services ordered by name.

func (*Store) ListAudit

func (s *Store) ListAudit(ctx context.Context, f AuditFilter) ([]AuditEvent, error)

ListAudit returns matching events, newest first. Filters AND-combine.

func (*Store) ListDocuments added in v0.8.1

func (s *Store) ListDocuments(ctx context.Context) ([]Document, error)

ListDocuments returns all documents ordered by name.

func (*Store) ListGroups

func (s *Store) ListGroups(ctx context.Context) ([]Group, error)

ListGroups returns all groups ordered by name.

func (*Store) ListGroupsForService

func (s *Store) ListGroupsForService(ctx context.Context, serviceID int64) ([]Group, error)

ListGroupsForService returns the groups granted access to the service.

func (*Store) ListServicesForGroups

func (s *Store) ListServicesForGroups(ctx context.Context, groups []string) ([]Service, error)

ListServicesForGroups returns the distinct services visible to any of the named groups, ordered by service name.

func (*Store) ListTrustedNetworks

func (s *Store) ListTrustedNetworks(ctx context.Context) ([]TrustedNetwork, error)

ListTrustedNetworks returns all trusted networks ordered by CIDR.

func (*Store) ListUsers

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

ListUsers returns all users ordered by username.

func (*Store) ListUsersInGroup

func (s *Store) ListUsersInGroup(ctx context.Context, groupID int64) ([]User, error)

ListUsersInGroup returns the group's members ordered by username.

func (*Store) LoadTrustedPrefixes

func (s *Store) LoadTrustedPrefixes(ctx context.Context) ([]netip.Prefix, error)

LoadTrustedPrefixes returns all trusted network prefixes for the live trust check. A per-accept DB read is fine: the data is human-cadence and tiny, and it means admin edits apply to new connections immediately.

func (*Store) PruneAudit

func (s *Store) PruneAudit(ctx context.Context, before time.Time) (int64, error)

PruneAudit deletes events strictly older than before; returns rows deleted.

func (*Store) RecordAudit

func (s *Store) RecordAudit(ctx context.Context, ev AuditEvent) error

RecordAudit inserts ev. At is stored as UTC RFC3339 (second resolution; the autoincrement id preserves insert order within a second).

func (*Store) RemoveUserFromGroup

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

RemoveUserFromGroup removes a membership (no-op if absent, mirroring AddUserToGroup's idempotency).

func (*Store) ResetAllMFA

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

ResetAllMFA wipes every stored secret (break-glass key recovery). It returns the number of users reset. mfa_required is preserved.

func (*Store) SetConfig

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

SetConfig updates the value for an existing catalog key in system_config. Returns ErrNotFound if the key does not exist (i.e. was never seeded).

func (*Store) SetDocument added in v0.8.1

func (s *Store) SetDocument(ctx context.Context, name, content, actor string) error

SetDocument replaces the named document's content, stamping who and when.

func (*Store) SetMFARequired

func (s *Store) SetMFARequired(ctx context.Context, userID int64, required bool) error

SetMFARequired sets the admin enforce flag. Returns ErrNotFound for an unknown user id.

func (*Store) SetMFASentinel

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

SetMFASentinel writes (inserts or replaces) the key-check sentinel.

func (*Store) SetPassword

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

SetPassword replaces the user's password hash. It exists because CreateUser is INSERT OR IGNORE and never updates an existing row. Returns ErrNotFound for an unknown user id.

func (*Store) SetUserSettingsLocked

func (s *Store) SetUserSettingsLocked(ctx context.Context, userID int64, locked bool) error

SetUserSettingsLocked toggles the admin lock that hides a user's self-service User Settings and freezes their MFA state as admin-managed. Returns ErrNotFound for an unknown user id.

func (*Store) StoreMFAEnrollment

func (s *Store) StoreMFAEnrollment(ctx context.Context, userID int64, encSecret, enrolledAt string, step int64) error

StoreMFAEnrollment arms MFA: it stores the encrypted secret, the enrollment timestamp, and the initial replay floor. Returns ErrNotFound for an unknown user id.

func (*Store) UnlinkGroupService

func (s *Store) UnlinkGroupService(ctx context.Context, groupID, serviceID int64) error

UnlinkGroupService revokes a group's access to a service (no-op if absent).

func (*Store) UpdateMFAStep

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

UpdateMFAStep advances the replay floor after a successful verification.

func (*Store) UpdateService

func (s *Store) UpdateService(ctx context.Context, id int64, name, description, host string, port int, tls, verify bool) error

UpdateService replaces every editable field of the service. Returns ErrNotFound for an unknown service id.

func (*Store) UpdateTrustedNetwork

func (s *Store) UpdateTrustedNetwork(ctx context.Context, id int64, cidr, comment string) error

UpdateTrustedNetwork replaces the CIDR and comment of an existing entry. Returns ErrNotFound for an unknown id.

func (*Store) UpdateUserDetails

func (s *Store) UpdateUserDetails(ctx context.Context, userID int64, fullName, email string) error

UpdateUserDetails replaces the user's optional display name and email. Email is normalized (trim + lower-case). Returns ErrNotFound for an unknown user id. The password is updated separately via SetPassword.

type TrustedNetwork

type TrustedNetwork struct {
	ID      int64
	CIDR    string
	Comment string
}

TrustedNetwork is a trusted client network record.

type User

type User struct {
	ID                 int64
	Username           string
	PasswordHash       string
	FullName           string
	Email              string
	MFARequired        bool
	MFASecret          string // AES-GCM ciphertext (base64); "" = not enrolled
	MFAEnrolledAt      string // UTC RFC3339; "" = not set
	MFALastStep        int64  // replay floor: highest accepted TOTP step
	UserSettingsLocked bool   // admin lock: hides self-service User Settings; freezes MFA as admin-managed
}

User is an account record.

Jump to

Keyboard shortcuts

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