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
- Variables
- func NormalizeDocName(name string) (string, error)
- func NormalizeEmail(email string) string
- func NormalizeServiceName(name string) (string, error)
- func ParseTrustedCIDR(s string) (netip.Prefix, error)
- func ReadDocumentFile(path string) (string, error)
- func ValidateComment(comment string) error
- func ValidateDescription(desc string) error
- func ValidateEmail(email string) error
- func ValidateFullName(name string) error
- type AuditEvent
- type AuditFilter
- type Document
- type Group
- type Service
- type Store
- func (s *Store) AddUserToGroup(ctx context.Context, userID, groupID int64) error
- func (s *Store) Backup(ctx context.Context, dest string) error
- func (s *Store) ClearMFA(ctx context.Context, userID int64) error
- func (s *Store) Close() error
- func (s *Store) CountAdminMembers(ctx context.Context) (int, error)
- func (s *Store) CountEnrolledUsers(ctx context.Context) (int, error)
- func (s *Store) CountGroupMembers(ctx context.Context, groupID int64) (int, error)
- func (s *Store) CountGroupServices(ctx context.Context, groupID int64) (int, error)
- func (s *Store) CreateGroup(ctx context.Context, name string) (int64, error)
- func (s *Store) CreateService(ctx context.Context, name, description, host string, port int, ...) (int64, error)
- func (s *Store) CreateTrustedNetwork(ctx context.Context, cidr, comment string) (int64, error)
- func (s *Store) CreateUser(ctx context.Context, username, passwordHash string) (int64, error)
- func (s *Store) DeleteGroup(ctx context.Context, groupID int64) error
- func (s *Store) DeleteService(ctx context.Context, serviceID int64) error
- func (s *Store) DeleteTrustedNetwork(ctx context.Context, id int64) error
- func (s *Store) DeleteUser(ctx context.Context, userID int64) error
- func (s *Store) GetConfig(ctx context.Context, key string) (string, error)
- func (s *Store) GetDocument(ctx context.Context, name string) (Document, error)
- func (s *Store) GetMFASentinel(ctx context.Context) (string, error)
- func (s *Store) GetService(ctx context.Context, id int64) (Service, error)
- func (s *Store) GetServiceByName(ctx context.Context, name string) (Service, error)
- func (s *Store) GetUserByUsername(ctx context.Context, username string) (User, error)
- func (s *Store) GetUserGroups(ctx context.Context, userID int64) ([]string, error)
- func (s *Store) LinkGroupService(ctx context.Context, groupID, serviceID int64) error
- func (s *Store) ListAllServices(ctx context.Context) ([]Service, error)
- func (s *Store) ListAudit(ctx context.Context, f AuditFilter) ([]AuditEvent, error)
- func (s *Store) ListDocuments(ctx context.Context) ([]Document, error)
- func (s *Store) ListGroups(ctx context.Context) ([]Group, error)
- func (s *Store) ListGroupsForService(ctx context.Context, serviceID int64) ([]Group, error)
- func (s *Store) ListServicesForGroups(ctx context.Context, groups []string) ([]Service, error)
- func (s *Store) ListTrustedNetworks(ctx context.Context) ([]TrustedNetwork, error)
- func (s *Store) ListUsers(ctx context.Context) ([]User, error)
- func (s *Store) ListUsersInGroup(ctx context.Context, groupID int64) ([]User, error)
- func (s *Store) LoadTrustedPrefixes(ctx context.Context) ([]netip.Prefix, error)
- func (s *Store) PruneAudit(ctx context.Context, before time.Time) (int64, error)
- func (s *Store) RecordAudit(ctx context.Context, ev AuditEvent) error
- func (s *Store) RemoveUserFromGroup(ctx context.Context, userID, groupID int64) error
- func (s *Store) ResetAllMFA(ctx context.Context) (int, error)
- func (s *Store) SetConfig(ctx context.Context, key, value string) error
- func (s *Store) SetDocument(ctx context.Context, name, content, actor string) error
- func (s *Store) SetMFARequired(ctx context.Context, userID int64, required bool) error
- func (s *Store) SetMFASentinel(ctx context.Context, value string) error
- func (s *Store) SetPassword(ctx context.Context, userID int64, passwordHash string) error
- func (s *Store) SetUserSettingsLocked(ctx context.Context, userID int64, locked bool) error
- func (s *Store) StoreMFAEnrollment(ctx context.Context, userID int64, encSecret, enrolledAt string, step int64) error
- func (s *Store) UnlinkGroupService(ctx context.Context, groupID, serviceID int64) error
- func (s *Store) UpdateMFAStep(ctx context.Context, userID int64, step int64) error
- func (s *Store) UpdateService(ctx context.Context, id int64, name, description, host string, port int, ...) error
- func (s *Store) UpdateTrustedNetwork(ctx context.Context, id int64, cidr, comment string) error
- func (s *Store) UpdateUserDetails(ctx context.Context, userID int64, fullName, email string) error
- type TrustedNetwork
- type User
Constants ¶
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.
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.
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.
const ( MaxServiceNameLen = 8 MaxDescriptionLen = 40 )
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.
const MaxCommentLen = 40
MaxCommentLen is the byte cap on a trusted network's operator annotation (matches the 40-char admin form field).
const MaxDocumentBytes = 8 << 10 // 8 KiB
MaxDocumentBytes caps a document's content (matches the old file read cap).
Variables ¶
var ErrDocumentTooLarge = errors.New("document too large (max 8 KiB)")
ErrDocumentTooLarge is returned when content (or an imported file) exceeds MaxDocumentBytes.
var ErrNotFound = errors.New("store: not found")
ErrNotFound is returned when a requested row does not exist.
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).
var KnownDocuments = []string{DocMOTD, DocBranding, DocHelpMenu}
KnownDocuments lists every valid document name.
Functions ¶
func NormalizeDocName ¶ added in v0.8.1
NormalizeDocName folds name to canonical uppercase and rejects names outside KnownDocuments (the single choke point, mirroring usernames/service names).
func NormalizeEmail ¶
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 ¶
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 ¶
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
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 ¶
ValidateComment enforces a required, length-bounded operator annotation.
func ValidateDescription ¶
ValidateDescription enforces a required, length-bounded service label.
func ValidateEmail ¶
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 ¶
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.
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 ¶
Open opens (creating if necessary) the SQLite database at path and applies the schema migration. The schema is idempotent.
func (*Store) AddUserToGroup ¶
AddUserToGroup links a user to a group (idempotent).
func (*Store) Backup ¶
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 ¶
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) CountAdminMembers ¶
CountAdminMembers returns the number of users in the AdminGroup (ZZADMIN).
func (*Store) CountEnrolledUsers ¶
CountEnrolledUsers returns how many users have a stored secret (used by the startup fail-closed check).
func (*Store) CountGroupMembers ¶
CountGroupMembers returns how many users belong to the group.
func (*Store) CountGroupServices ¶
CountGroupServices returns how many services the group can access.
func (*Store) CreateGroup ¶
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 ¶
CreateTrustedNetwork inserts a trusted network, or returns the existing record's id if the CIDR already exists (idempotent, mirrors CreateService).
func (*Store) CreateUser ¶
CreateUser inserts a user, or returns the existing user's id if the username already exists (idempotent for seeding).
func (*Store) DeleteGroup ¶
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 ¶
DeleteService removes the service and its group links in one transaction.
func (*Store) DeleteTrustedNetwork ¶
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 ¶
DeleteUser removes the user and its group memberships in one transaction.
func (*Store) GetConfig ¶
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
GetDocument returns the named document, or ErrNotFound for a known name whose row is missing (cannot happen after reconcileDefaults; defensive).
func (*Store) GetMFASentinel ¶
GetMFASentinel returns the stored key-check sentinel, or "" if none exists.
func (*Store) GetService ¶
GetService returns the service by id, or ErrNotFound.
func (*Store) GetServiceByName ¶
GetServiceByName returns the service by name (case-insensitive), or ErrNotFound.
func (*Store) GetUserByUsername ¶
GetUserByUsername returns the user, or ErrNotFound.
func (*Store) GetUserGroups ¶
GetUserGroups returns the names of groups the user belongs to.
func (*Store) LinkGroupService ¶
LinkGroupService grants a group access to a service (idempotent).
func (*Store) ListAllServices ¶
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
ListDocuments returns all documents ordered by name.
func (*Store) ListGroups ¶
ListGroups returns all groups ordered by name.
func (*Store) ListGroupsForService ¶
ListGroupsForService returns the groups granted access to the service.
func (*Store) ListServicesForGroups ¶
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) ListUsersInGroup ¶
ListUsersInGroup returns the group's members ordered by username.
func (*Store) LoadTrustedPrefixes ¶
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 ¶
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 ¶
RemoveUserFromGroup removes a membership (no-op if absent, mirroring AddUserToGroup's idempotency).
func (*Store) ResetAllMFA ¶
ResetAllMFA wipes every stored secret (break-glass key recovery). It returns the number of users reset. mfa_required is preserved.
func (*Store) SetConfig ¶
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
SetDocument replaces the named document's content, stamping who and when.
func (*Store) SetMFARequired ¶
SetMFARequired sets the admin enforce flag. Returns ErrNotFound for an unknown user id.
func (*Store) SetMFASentinel ¶
SetMFASentinel writes (inserts or replaces) the key-check sentinel.
func (*Store) SetPassword ¶
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 ¶
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 ¶
UnlinkGroupService revokes a group's access to a service (no-op if absent).
func (*Store) UpdateMFAStep ¶
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 ¶
UpdateTrustedNetwork replaces the CIDR and comment of an existing entry. Returns ErrNotFound for an unknown id.
func (*Store) UpdateUserDetails ¶
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 ¶
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.