Documentation
¶
Overview ¶
Package sqlcommon implements storage.Store over database/sql using a caller-selected dialect.
Design ¶
Shared queries and transaction shapes cover enrollment, command queues, certificate history, push certificates, user-authentication state and migration. Dialects supply fixed SQL syntax and migrations; data values use parameters. Selected secret columns are sealed through storage/crypt, and Rewrap rotates them in guarded batches.
The driver packages open connections. Satellite domain stores reuse migration/dialect support while owning their own migration sets. Operations across different domain stores are not a distributed transaction.
References ¶
- Decision record 0012: https://github.com/deploymenttheory/go-apple-dm/blob/main/docs/research/decisions/0012-sql-storage-backends.md
- Decision record 0013: https://github.com/deploymenttheory/go-apple-dm/blob/main/docs/research/decisions/0013-secrets-at-rest.md
- Decision record 0014: https://github.com/deploymenttheory/go-apple-dm/blob/main/docs/research/decisions/0014-cert-association-history.md
- Decision record 0015: https://github.com/deploymenttheory/go-apple-dm/blob/main/docs/research/decisions/0015-push-cert-store.md
- Decision record 0016: https://github.com/deploymenttheory/go-apple-dm/blob/main/docs/research/decisions/0016-user-authenticate-state.md
- Decision record 0017: https://github.com/deploymenttheory/go-apple-dm/blob/main/docs/research/decisions/0017-enrollment-export-import.md
- Threat model: https://github.com/deploymenttheory/go-apple-dm/blob/main/docs/security/threat-model.md (Storage rows)
- Apple: https://developer.apple.com/documentation/devicemanagement/check-in
- Apple: https://developer.apple.com/documentation/devicemanagement/handling-notnow-status-responses
Index ¶
- Constants
- Variables
- func AfterCommit(ctx context.Context, fn func(context.Context))
- func AfterCompletion(ctx context.Context, fn func(context.Context, bool))
- func BlobAAD(purpose string, keys ...string) []byte
- func CurrentTransaction(ctx context.Context, db *sql.DB) (*sql.Tx, bool)
- func Fail(ctx context.Context, err error) error
- func InsertIgnoreDuplicateKey(table string, cols, key []string) string
- func InsertIgnoreOnConflict(table string, cols, key []string) string
- func Migrate(ctx context.Context, db *sql.DB, d Dialect) ([]int, error)
- func MigrateSet(ctx context.Context, db *sql.DB, d Dialect, set MigrationSet) ([]int, error)
- func MustSub(fsys fs.FS, dir string) fs.FS
- func OpenBlob(k *crypt.Keyring, purpose string, b []byte, keys ...string) ([]byte, error)
- func PutPushCertTx(ctx context.Context, q interface{ ... }, d Dialect, keys *crypt.Keyring, ...) (storage.PushCert, error)
- func RewrapBlobs(ctx context.Context, db *sql.DB, d Dialect, k *crypt.Keyring, ...) (int, error)
- func Rollback(ctx context.Context, db *sql.DB, d Dialect, target int) ([]int, error)
- func RollbackSet(ctx context.Context, db *sql.DB, d Dialect, set MigrationSet, target int) ([]int, error)
- func Savepoint(ctx context.Context, db *sql.DB, fn func(context.Context, *sql.Tx) error) (err error)
- func SealBlob(k *crypt.Keyring, purpose string, b []byte, keys ...string) ([]byte, error)
- func UpsertOnConflict(table string, cols, key []string) string
- func UpsertOnDuplicateKey(table string, cols, _ []string) string
- func Version(ctx context.Context, db *sql.DB, d Dialect) (int, error)
- func VersionOf(ctx context.Context, db *sql.DB, d Dialect, table string) (int, error)
- type BlobColumn
- type Dialect
- type Migration
- type MigrationSet
- type Option
- type Pool
- type Queryer
- type Store
- func (s *Store) AssociateCert(ctx context.Context, id mdm.EnrollmentID, hash string, at time.Time) error
- func (s *Store) AuthenticateEnrollment(ctx context.Context, id mdm.EnrollmentID, c storage.AuthenticateChange) error
- func (s *Store) BootstrapToken(ctx context.Context, id mdm.EnrollmentID) ([]byte, error)
- func (s *Store) CertHash(ctx context.Context, id mdm.EnrollmentID) (string, error)
- func (s *Store) CertHashHistory(ctx context.Context, hash string) ([]storage.CertAssociation, error)
- func (s *Store) CertHistory(ctx context.Context, id mdm.EnrollmentID) ([]storage.CertAssociation, error)
- func (s *Store) Clear(ctx context.Context, id mdm.EnrollmentID, f storage.ClearFilter) (int64, error)
- func (s *Store) ClearCommand(ctx context.Context, id mdm.EnrollmentID, uuid string) (int64, error)
- func (s *Store) ClearUserAuth(ctx context.Context, id mdm.EnrollmentID) error
- func (s *Store) Close() error
- func (s *Store) Commands(ctx context.Context, id mdm.EnrollmentID, q storage.CommandQuery, ...) (paging.Result[storage.QueuedCommand], error)
- func (s *Store) DB() *sql.DB
- func (s *Store) Disable(ctx context.Context, id mdm.EnrollmentID, at time.Time) error
- func (s *Store) Enqueue(ctx context.Context, ids []mdm.EnrollmentID, cmd *mdm.Command, ...) (storage.EnqueueResult, error)
- func (s *Store) EnrollmentByCertHash(ctx context.Context, hash string) (mdm.EnrollmentID, error)
- func (s *Store) EnrollmentByID(ctx context.Context, id string) (*storage.Enrollment, error)
- func (s *Store) Export(ctx context.Context, p paging.Page) (paging.Result[storage.EnrollmentExport], error)
- func (s *Store) Get(ctx context.Context, id mdm.EnrollmentID) (*storage.Enrollment, error)
- func (s *Store) Import(ctx context.Context, rec storage.EnrollmentExport) error
- func (s *Store) List(ctx context.Context, q storage.EnrollmentQuery, p paging.Page) (paging.Result[storage.Enrollment], error)
- func (s *Store) Next(ctx context.Context, id mdm.EnrollmentID, skipNotNow bool, now time.Time) (*mdm.Command, error)
- func (s *Store) Ping(ctx context.Context) error
- func (s *Store) PushCert(ctx context.Context, topic string) (*storage.PushCert, error)
- func (s *Store) PushCertVersion(ctx context.Context, topic string) (int64, error)
- func (s *Store) PushCerts(ctx context.Context) ([]storage.PushCert, error)
- func (s *Store) PushInfo(ctx context.Context, ids []mdm.EnrollmentID) (map[mdm.EnrollmentID]mdm.Push, error)
- func (s *Store) Rewrap(ctx context.Context) (int, error)
- func (s *Store) StoreBootstrapToken(ctx context.Context, id mdm.EnrollmentID, token []byte, at time.Time) error
- func (s *Store) StorePushCert(ctx context.Context, topic string, certPEM, keyPEM []byte, at time.Time) (storage.PushCert, error)
- func (s *Store) StoreResult(ctx context.Context, id mdm.EnrollmentID, resp *mdm.Response, now time.Time) error
- func (s *Store) StoreTokenUpdate(ctx context.Context, id mdm.EnrollmentID, push mdm.Push, ...) error
- func (s *Store) StoreUserAuthChallenge(ctx context.Context, id mdm.EnrollmentID, challenge string, raw []byte, ...) error
- func (s *Store) StoreUserAuthToken(ctx context.Context, id mdm.EnrollmentID, token string, raw []byte, ...) error
- func (s *Store) TouchLastSeen(ctx context.Context, id mdm.EnrollmentID, at time.Time) error
- func (s *Store) TransitionReplacement(ctx context.Context, id mdm.EnrollmentID, change storage.ReplacementChange) (*storage.Replacement, error)
- func (s *Store) UpsertAuthenticate(ctx context.Context, id mdm.EnrollmentID, msg *checkin.Authenticate, ...) error
- func (s *Store) UserAuth(ctx context.Context, id mdm.EnrollmentID) (*storage.UserAuthState, error)
- type UnitOfWork
Constants ¶
const ClearBatchSize = 5000
ClearBatchSize bounds each Clear statement to limit lock duration. The PostgreSQL performance test measures clearing 100,000 rows; timing depends on the database and host.
const DefaultMigrationsTable = "schema_migrations"
DefaultMigrationsTable records the storage schema's applied versions.
const RewrapBatchSize = 500
RewrapBatchSize bounds the rows read per SELECT during Rewrap.
Variables ¶
var ErrMigration = errors.New("sqlcommon: migration")
ErrMigration is returned for malformed or failing migrations.
var ErrTransaction = errors.New("sqlcommon: transaction coordination failed")
Functions ¶
func AfterCommit ¶
AfterCommit schedules an in-process notification after commit. It runs immediately outside a transaction. Its context has no transaction or request cancellation, so the receiver must impose its own bounded lifetime.
func AfterCompletion ¶
AfterCompletion schedules work outside the transaction on either outcome. Security denials can use this to record an occurrence even after rollback.
func CurrentTransaction ¶
CurrentTransaction reports the transaction for db in this context.
func Fail ¶
Fail prevents a transaction from committing, even when an intermediate caller ignores the returned error. Outside a transaction it returns err unchanged.
func InsertIgnoreDuplicateKey ¶
InsertIgnoreDuplicateKey renders INSERT ... AS new ON DUPLICATE KEY UPDATE k = new.k, a no-op update on conflict (MySQL 8.0.19 and later).
func InsertIgnoreOnConflict ¶
InsertIgnoreOnConflict renders INSERT ... ON CONFLICT (key) DO NOTHING (PostgreSQL and SQLite).
func Migrate ¶
Migrate applies every pending migration of the dialect's own set in order, each in its own transaction, and returns the versions applied.
func MigrateSet ¶
MigrateSet is Migrate for an arbitrary migration set and version table.
func MustSub ¶
MustSub returns the sub-tree dir of fsys, panicking when it does not exist. Backends use it to expose their embedded migration directory as a package-level Dialect value.
func PutPushCertTx ¶
func PutPushCertTx(ctx context.Context, q interface { ExecContext(context.Context, string, ...any) (sql.Result, error) QueryRowContext(context.Context, string, ...any) *sql.Row }, d Dialect, keys *crypt.Keyring, topic string, certPEM, keyPEM []byte, at time.Time) (storage.PushCert, error)
PutPushCertTx publishes an identity in the caller's transaction. It allows certificate workflow state and the runtime push record to commit together.
func RewrapBlobs ¶
func RewrapBlobs( ctx context.Context, db *sql.DB, d Dialect, k *crypt.Keyring, cols []BlobColumn, ) (int, error)
RewrapBlobs rotates bounded pages with compare-and-swap writes. Call until zero before removing retired keys; concurrent deletions can move page boundaries.
func Rollback ¶
Rollback reverts applied migrations of the dialect's own set newer than target (0 reverts all), newest first, each in its own transaction.
func RollbackSet ¶
func RollbackSet(ctx context.Context, db *sql.DB, d Dialect, set MigrationSet, target int) ([]int, error)
RollbackSet is Rollback for an arbitrary migration set and version table.
func Savepoint ¶
func Savepoint(ctx context.Context, db *sql.DB, fn func(context.Context, *sql.Tx) error) (err error)
Savepoint preserves a participating store's Update contract inside an outer transaction. An ordinary callback error rolls back only that store operation; Fail still poisons the entire transaction. Callbacks must use the supplied context, and nested SQL operations must run sequentially.
func UpsertOnConflict ¶
UpsertOnConflict renders INSERT ... ON CONFLICT (key) DO UPDATE SET col = excluded.col (PostgreSQL and SQLite).
func UpsertOnDuplicateKey ¶
UpsertOnDuplicateKey renders INSERT ... AS new ON DUPLICATE KEY UPDATE col = new.col (MySQL 8.0.19 and later).
Types ¶
type BlobColumn ¶
BlobColumn identifies a retained blob and its ordered primary-key columns. Identifiers must be compile-time schema constants, never request input.
type Dialect ¶
type Dialect struct {
// Name is used in errors and in the migration table.
Name string
// Dollar selects $1-style placeholders (PostgreSQL); otherwise "?".
Dollar bool
// ForUpdate is appended to row-locking SELECTs inside transactions
// ("FOR UPDATE"), or empty when the engine serialises writers (SQLite).
ForUpdate string
// Upsert renders an INSERT that updates the non-key columns on a key
// conflict. See UpsertOnConflict and UpsertOnDuplicateKey.
Upsert func(table string, cols, key []string) string
// Migrations holds NNNN_name.sql files with "-- +up" and "-- +down"
// sections.
Migrations fs.FS
// InsertIgnore renders an INSERT that does nothing when the key already
// exists. See InsertIgnoreOnConflict and InsertIgnoreDuplicateKey.
InsertIgnore func(table string, cols, key []string) string
// IsUniqueViolation reports whether err is the engine's unique-constraint
// error, so the store returns storage.ErrConflict instead of a driver
// error when two writers race for the same key.
IsUniqueViolation func(error) bool
}
Dialect describes what differs between SQL engines.
type MigrationSet ¶
type MigrationSet struct {
// Table is the version table, for example "schema_migrations" or
// "ddm_schema_migrations". Lower-case identifiers only.
Table string
FS fs.FS
}
MigrationSet is one package's versioned migrations and the table that records them, so several packages (storage, ddm) can share a database without sharing a version sequence.
type Option ¶
type Option func(*Store)
Option configures New.
func WithKeyring ¶
WithKeyring seals the secret columns (unlock tokens, bootstrap tokens, push private keys, user auth tokens) with the keyring. Without one the columns are stored in plaintext, which is the pre-0013 behaviour.
type Pool ¶
type Pool struct {
MaxOpenConns int
MaxIdleConns int
ConnMaxLifetime time.Duration
ConnMaxIdleTime time.Duration
}
Pool tunes the database/sql connection pool. Zero values keep the driver defaults.
type Queryer ¶
type Queryer interface {
ExecContext(context.Context, string, ...any) (sql.Result, error)
QueryContext(context.Context, string, ...any) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...any) *sql.Row
}
Queryer is a pool or the transaction shared by stores participating in Run.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store implements storage.Store over a *sql.DB.
func (*Store) AssociateCert ¶
func (s *Store) AssociateCert( ctx context.Context, id mdm.EnrollmentID, hash string, at time.Time, ) error
AssociateCert implements storage.CertAuthStore. The unique index on cert_hash is the arbiter: a race between two enrollments for one hash ends with one winner and ErrConflict for the rest. The pin and its history row are written in one transaction (decision record 0014).
func (*Store) AuthenticateEnrollment ¶
func (s *Store) AuthenticateEnrollment( ctx context.Context, id mdm.EnrollmentID, c storage.AuthenticateChange, ) error
AuthenticateEnrollment implements storage.EnrollmentStore.
func (*Store) BootstrapToken ¶
BootstrapToken implements storage.BootstrapTokenStore.
func (*Store) CertHashHistory ¶
func (s *Store) CertHashHistory(ctx context.Context, hash string) ([]storage.CertAssociation, error)
CertHashHistory implements storage.CertAuthStore.
func (*Store) CertHistory ¶
func (s *Store) CertHistory( ctx context.Context, id mdm.EnrollmentID, ) ([]storage.CertAssociation, error)
CertHistory implements storage.CertAuthStore.
func (*Store) Clear ¶
func (s *Store) Clear( ctx context.Context, id mdm.EnrollmentID, f storage.ClearFilter, ) (int64, error)
Clear implements storage.CommandQueue in indexed batches of ClearBatchSize rows. Each batch is its own statement, so a failure part way through returns the count applied so far; callers may simply retry.
func (*Store) ClearCommand ¶
ClearCommand implements storage.CommandClearer.
func (*Store) ClearUserAuth ¶
ClearUserAuth implements storage.UserAuthStore.
func (*Store) Commands ¶
func (s *Store) Commands( ctx context.Context, id mdm.EnrollmentID, q storage.CommandQuery, p paging.Page, ) (paging.Result[storage.QueuedCommand], error)
Commands implements storage.CommandQueue with a keyset cursor on the sequence number, newest first.
func (*Store) Disable ¶
Disable implements storage.EnrollmentStore. Disabling a device channel also disables its user channels, in one transaction.
func (*Store) Enqueue ¶
func (s *Store) Enqueue( ctx context.Context, ids []mdm.EnrollmentID, cmd *mdm.Command, o storage.EnqueueOptions, ) (storage.EnqueueResult, error)
Enqueue implements storage.CommandQueue.
func (*Store) EnrollmentByCertHash ¶
EnrollmentByCertHash implements storage.CertAuthStore.
func (*Store) EnrollmentByID ¶
EnrollmentByID implements storage.EnrollmentStore.
func (*Store) Export ¶
func (s *Store) Export( ctx context.Context, p paging.Page, ) (paging.Result[storage.EnrollmentExport], error)
Export implements storage.MigrationStore (decision record 0017): device channels come before the user channels that belong to them because the order is (parent_id, id) and device rows have an empty parent. Each device row costs two extra reads (bootstrap token and history); this is an administrative operation, not a hot path.
func (*Store) Get ¶
func (s *Store) Get(ctx context.Context, id mdm.EnrollmentID) (*storage.Enrollment, error)
Get implements storage.EnrollmentStore.
func (*Store) List ¶
func (s *Store) List( ctx context.Context, q storage.EnrollmentQuery, p paging.Page, ) (paging.Result[storage.Enrollment], error)
List implements storage.EnrollmentStore with a keyset cursor on id.
func (*Store) Next ¶
func (s *Store) Next( ctx context.Context, id mdm.EnrollmentID, skipNotNow bool, now time.Time, ) (*mdm.Command, error)
Next implements storage.CommandQueue.
func (*Store) PushCertVersion ¶
PushCertVersion implements storage.PushCertStore.
func (*Store) PushInfo ¶
func (s *Store) PushInfo( ctx context.Context, ids []mdm.EnrollmentID, ) (map[mdm.EnrollmentID]mdm.Push, error)
PushInfo implements storage.PushStore.
func (*Store) Rewrap ¶
Rewrap re-seals every value that is unsealed or sealed under a retired key so it uses the active key, and returns how many rows it rewrote. Rows changed by another writer between read and write are skipped, so callers loop until it returns 0.
func (*Store) StoreBootstrapToken ¶
func (s *Store) StoreBootstrapToken( ctx context.Context, id mdm.EnrollmentID, token []byte, at time.Time, ) error
StoreBootstrapToken implements storage.BootstrapTokenStore.
func (*Store) StorePushCert ¶
func (s *Store) StorePushCert(ctx context.Context, topic string, certPEM, keyPEM []byte, at time.Time) (storage.PushCert, error)
StorePushCert implements storage.PushCertStore (decision record 0015). The private key is sealed when a keyring is configured.
func (*Store) StoreResult ¶
func (s *Store) StoreResult( ctx context.Context, id mdm.EnrollmentID, resp *mdm.Response, now time.Time, ) error
StoreResult implements storage.CommandQueue.
func (*Store) StoreTokenUpdate ¶
func (s *Store) StoreTokenUpdate( ctx context.Context, id mdm.EnrollmentID, push mdm.Push, msg *checkin.TokenUpdate, raw []byte, at time.Time, ) error
StoreTokenUpdate implements storage.EnrollmentStore.
func (*Store) StoreUserAuthChallenge ¶
func (s *Store) StoreUserAuthChallenge( ctx context.Context, id mdm.EnrollmentID, challenge string, raw []byte, at time.Time, ) error
StoreUserAuthChallenge implements storage.UserAuthStore.
func (*Store) StoreUserAuthToken ¶
func (s *Store) StoreUserAuthToken( ctx context.Context, id mdm.EnrollmentID, token string, raw []byte, at time.Time, ) error
StoreUserAuthToken implements storage.UserAuthStore.
func (*Store) TouchLastSeen ¶
TouchLastSeen implements storage.EnrollmentStore; it never moves the timestamp backwards.
func (*Store) TransitionReplacement ¶
func (s *Store) TransitionReplacement( ctx context.Context, id mdm.EnrollmentID, change storage.ReplacementChange, ) (*storage.Replacement, error)
func (*Store) UpsertAuthenticate ¶
func (s *Store) UpsertAuthenticate( ctx context.Context, id mdm.EnrollmentID, msg *checkin.Authenticate, raw []byte, at time.Time, ) error
UpsertAuthenticate implements storage.EnrollmentStore.
func (*Store) UserAuth ¶
func (s *Store) UserAuth(ctx context.Context, id mdm.EnrollmentID) (*storage.UserAuthState, error)
UserAuth implements storage.UserAuthStore.
type UnitOfWork ¶
UnitOfWork coordinates stores using the same pool. Configure every store with that pool and use Query or CurrentTransaction for each operation. A callback must finish all database work before returning; it must not retain its context for background work or perform a remote side effect inside the transaction.