Documentation
¶
Overview ¶
Package storage defines the persistence interfaces the service layer uses, split by concern: enrollments, push tokens, the command queue, bootstrap tokens, certificate associations, push certificates, UserAuthenticate state, and export and import, with Page and cursor types and sentinel errors.
Why ¶
An enrollment is the sum of what Authenticate, TokenUpdate, and SetBootstrapToken delivered, plus the commands queued for it, and the service must be able to keep that on any database an operator runs. Phase 2 of the plan of record fixes the contract here (decision record 0005) so the service is written once against interfaces and every backend proves itself against the storagetest suites. Later records add interfaces to the same package rather than new ones: certificate association history (0014), the push certificate store (0015), UserAuthenticate state (0016), and export and import (0017). Store composes them all.
Backends live in sub-packages (inmem, sqlite, postgres, mysql via sqlcommon) and encryption of stored secrets in storage/crypt; this package holds no implementation beyond the NotNow backoff schedule.
References ¶
- Decision record 0005: docs/research/decisions/0005-storage-interfaces.md
- Decision record 0013: docs/research/decisions/0013-secrets-at-rest.md
- Decision record 0014: docs/research/decisions/0014-cert-association-history.md
- Decision record 0015: docs/research/decisions/0015-push-cert-store.md
- Decision record 0016: docs/research/decisions/0016-user-authenticate-state.md
- Decision record 0017: docs/research/decisions/0017-enrollment-export-import.md
- Plan of record: docs/research/implementation_plan.md (section 3, core domain model; phase 2)
- Threat model: docs/security/threat-model.md (Storage rows)
- End-to-end scenarios: docs/testing/e2e-scenarios.md (E2E-003, E2E-005)
- Apple: https://developer.apple.com/documentation/devicemanagement/check-in
- Apple: https://developer.apple.com/documentation/devicemanagement/sending-mdm-commands-to-a-device
- Apple: https://developer.apple.com/documentation/devicemanagement/handling-notnow-status-responses
- Schema: third_party/device-management/mdm/checkin/authenticate.yaml, tokenupdate.yaml, setbootstraptoken.yaml
Index ¶
- Constants
- Variables
- func NotNowBackoff(attempt int) time.Duration
- type BootstrapTokenStore
- type CertAssociation
- type CertAuthStore
- type ClearFilter
- type CommandQuery
- type CommandQueue
- type DeviceInfo
- type EnqueueOptions
- type EnqueueResult
- type Enrollment
- type EnrollmentExport
- type EnrollmentQuery
- type EnrollmentStore
- type MigrationStore
- type Page
- type PushCert
- type PushCertStore
- type PushStore
- type QueuedCommand
- type Result
- type State
- type Store
- type UserAuthState
- type UserAuthStore
Constants ¶
const DefaultPageSize = 100
DefaultPageSize applies when Page.Limit is not positive.
Variables ¶
var ( ErrNotFound = errors.New("storage: not found") ErrDisabled = errors.New("storage: enrollment disabled") ErrConflict = errors.New("storage: conflict") ErrInvalid = errors.New("storage: invalid argument") )
Errors shared by every backend.
var ErrUserChannelRequired = errors.New("storage: user channel required")
ErrUserChannelRequired is wrapped in ErrInvalid by UserAuthStore methods called with a device channel.
Functions ¶
func NotNowBackoff ¶
NotNowBackoff is the default retry delay after the nth NotNow (1-based): 30s, 1m, 2m, 4m, ... capped at 1h.
Types ¶
type BootstrapTokenStore ¶
type BootstrapTokenStore interface {
// StoreBootstrapToken escrows token for the device channel of id and
// records at as Enrollment.BootstrapTokenAt.
StoreBootstrapToken(ctx context.Context, id mdm.EnrollmentID, token []byte, at time.Time) error
// BootstrapToken returns the token or ErrNotFound.
BootstrapToken(ctx context.Context, id mdm.EnrollmentID) ([]byte, error)
}
BootstrapTokenStore escrows macOS bootstrap tokens (device channel).
type CertAssociation ¶
type CertAssociation struct {
ID mdm.EnrollmentID
Hash string
At time.Time
}
CertAssociation is one row of the append-only pin history: a device channel pinned a certificate hash at a time (decision record 0014).
type CertAuthStore ¶
type CertAuthStore interface {
// AssociateCert pins hash to the device channel of id at the given time
// and appends the pair to the history. ErrConflict when the hash is
// currently pinned to a different enrollment, including when two
// callers race to pin the same hash.
AssociateCert(ctx context.Context, id mdm.EnrollmentID, hash string, at time.Time) error
// CertHash returns the pinned hash for the device channel of id, or
// "" when none.
CertHash(ctx context.Context, id mdm.EnrollmentID) (string, error)
// EnrollmentByCertHash resolves a hash to the device-channel enrollment
// that currently pins it.
EnrollmentByCertHash(ctx context.Context, hash string) (mdm.EnrollmentID, error)
// CertHistory returns every hash ever pinned to the device channel of
// id, oldest first. It is empty, not ErrNotFound, for an enrollment
// that never pinned; ErrNotFound for an unknown enrollment.
CertHistory(ctx context.Context, id mdm.EnrollmentID) ([]CertAssociation, error)
// CertHashHistory returns every enrollment that ever pinned hash,
// oldest first; empty when the hash was never seen.
CertHashHistory(ctx context.Context, hash string) ([]CertAssociation, error)
}
CertAuthStore pins identity certificates to device-channel enrollments and keeps the history of every pin.
type ClearFilter ¶
type ClearFilter struct {
States []State // default: every non-terminal state
RequestType string
Before time.Time // enqueued before this time
}
ClearFilter selects commands for Clear. Zero values mean "any".
type CommandQuery ¶
CommandQuery filters Commands.
type CommandQueue ¶
type CommandQueue interface {
// Enqueue queues cmd for each enrollment. Disabled or unknown
// enrollments are reported in Skipped, not as an error.
Enqueue(ctx context.Context, ids []mdm.EnrollmentID, cmd *mdm.Command, o EnqueueOptions) (EnqueueResult, error)
// Next returns the next command to deliver, in enqueue order: pending
// and sent commands, plus NotNow commands whose backoff elapsed unless
// skipNotNow is set (the device just said NotNow). It marks the command
// sent. nil, nil when the queue is empty.
Next(ctx context.Context, id mdm.EnrollmentID, skipNotNow bool, now time.Time) (*mdm.Command, error)
// StoreResult records the device's response for the command it names.
// Unknown CommandUUIDs return ErrNotFound.
StoreResult(ctx context.Context, id mdm.EnrollmentID, resp *mdm.Response, now time.Time) error
// Commands pages through an enrollment's commands, newest first.
Commands(ctx context.Context, id mdm.EnrollmentID, q CommandQuery, p Page) (Result[QueuedCommand], error)
// Clear marks matching non-terminal commands cleared and returns how
// many. Backends may apply it in batches without one enclosing
// transaction: on error the count is what was applied so far and the
// caller may simply retry.
Clear(ctx context.Context, id mdm.EnrollmentID, f ClearFilter) (int64, error)
}
CommandQueue persists commands per enrollment.
type DeviceInfo ¶
type DeviceInfo struct {
SerialNumber string
Model string
ModelName string
DeviceName string
ProductName string
OSVersion string
BuildVersion string
IMEI string
MEID string
Topic string
}
DeviceInfo is the subset of Authenticate worth indexing.
func DeviceInfoFromAuthenticate ¶
func DeviceInfoFromAuthenticate(m *checkin.Authenticate) DeviceInfo
DeviceInfoFromAuthenticate extracts the indexed fields.
type EnqueueOptions ¶
type EnqueueOptions struct {
// DedupeKey skips enrollments that already have a non-terminal command
// with the same key (for example one DeclarativeManagement kick).
DedupeKey string
// Now stamps EnqueuedAt; zero means time.Now().
Now time.Time
}
EnqueueOptions tune Enqueue.
type EnqueueResult ¶
type EnqueueResult struct {
Queued []mdm.EnrollmentID
Skipped map[mdm.EnrollmentID]error
}
EnqueueResult reports per-enrollment outcomes.
type Enrollment ¶
type Enrollment struct {
ID mdm.EnrollmentID
// Enabled becomes true on TokenUpdate and false on CheckOut or a new
// Authenticate; only enabled enrollments receive commands and pushes.
Enabled bool
Push mdm.Push
Device DeviceInfo
// User channel fields from TokenUpdate: names, whether the user is
// logged in without console access, and the EnrollmentUserID of a
// User Enrollment's user channel (decision record 0029).
UserShortName string
UserLongName string
NotOnConsole bool
EnrollmentUserID string
// UnlockToken from TokenUpdate (macOS), if the device sent one.
UnlockToken []byte
// AuthenticateRaw is the last Authenticate plist as received.
AuthenticateRaw []byte
// TokenUpdateRaw is the last TokenUpdate plist as received, kept so an
// enrollment can be replayed into another server (decision record 0017).
TokenUpdateRaw []byte
EnrolledAt time.Time
TokenUpdatedAt time.Time
LastSeenAt time.Time
DisabledAt time.Time
// CertHash is the pinned identity certificate fingerprint (device channels).
CertHash string
// CertHashAt is when CertHash was pinned (zero when none).
CertHashAt time.Time
// BootstrapTokenAt is when the escrowed bootstrap token was stored
// (zero when none). The token itself is read through BootstrapTokenStore.
BootstrapTokenAt time.Time
}
Enrollment is one channel of one enrollment as the server knows it.
type EnrollmentExport ¶
type EnrollmentExport struct {
Enrollment
BootstrapToken []byte
CertHistory []CertAssociation
}
EnrollmentExport is everything one enrollment channel needs to move to another backend (decision record 0017). Empty byte fields are nil.
type EnrollmentQuery ¶
type EnrollmentQuery struct {
Channel mdm.Channel
Enabled *bool
ParentID string
// Serial matches Device.SerialNumber exactly. The SQL backends index
// this column, so it is a lookup rather than a scan.
Serial string
}
EnrollmentQuery filters List. Zero values mean "any".
type EnrollmentStore ¶
type EnrollmentStore interface {
// UpsertAuthenticate records an Authenticate message. It creates the
// record or resets an existing one: push info, unlock token, bootstrap
// token, certificate association, and the pending command queue are
// cleared so a re-enrollment never inherits the previous identity's
// state. The enrollment stays disabled until TokenUpdate.
UpsertAuthenticate(ctx context.Context, id mdm.EnrollmentID, msg *checkin.Authenticate, raw []byte, at time.Time) error
// StoreTokenUpdate records push info, the raw plist, and enables the
// enrollment. An unlock token in msg replaces the stored one; a missing
// one keeps it.
StoreTokenUpdate(ctx context.Context, id mdm.EnrollmentID, push mdm.Push, msg *checkin.TokenUpdate, raw []byte, at time.Time) error
// Disable marks the enrollment as checked out. Disabling a device
// channel also disables the user channels whose parent it is, because a
// checked-out device cannot carry a user channel. Records are kept.
Disable(ctx context.Context, id mdm.EnrollmentID, at time.Time) error
// Get returns the record or ErrNotFound.
Get(ctx context.Context, id mdm.EnrollmentID) (*Enrollment, error)
// List pages through enrollments ordered by id.
List(ctx context.Context, q EnrollmentQuery, p Page) (Result[Enrollment], error)
// TouchLastSeen records device activity.
TouchLastSeen(ctx context.Context, id mdm.EnrollmentID, at time.Time) error
}
EnrollmentStore persists enrollment records.
type MigrationStore ¶
type MigrationStore interface {
// Export pages through every enrollment with device channels before the
// user channels that belong to them.
Export(ctx context.Context, p Page) (Result[EnrollmentExport], error)
// Import writes rec exactly as given (Enabled, timestamps, pin, tokens,
// history) in one transaction, upserting by id. ErrInvalid for a user
// channel whose parent is absent or for history rows naming another
// enrollment; ErrConflict when CertHash is currently pinned elsewhere.
// The command queue is not touched.
Import(ctx context.Context, rec EnrollmentExport) error
}
MigrationStore exports and imports enrollment records between backends.
type Page ¶ added in v1.0.0
Page requests one page of results. An empty Cursor starts from the beginning; Limit <= 0 uses the backend default.
type PushCert ¶
type PushCert struct {
Topic string
CertPEM []byte
KeyPEM []byte
NotAfter time.Time
// Version increments on every StorePushCert for the topic, so caches
// can detect a renewal with one cheap read.
Version int64
UpdatedAt time.Time
}
PushCert is a stored APNs push certificate for one topic (decision record 0015). KeyPEM is empty in listings.
func ValidatePushCert ¶
ValidatePushCert checks a PEM certificate and key pair the way every backend's StorePushCert must: the key matches the certificate, the subject carries an APNs topic, the topic matches when one is given, and the certificate is valid at the given time. It returns the record to store, with copies of the PEM bytes and Version unset.
type PushCertStore ¶
type PushCertStore interface {
// StorePushCert validates the PEM pair (key matches certificate, topic
// in the subject UID, not expired at the given time) and upserts it.
// An empty topic accepts the certificate's own topic; otherwise the two
// must match. ErrInvalid for anything that fails validation. The
// returned record carries the new Version and no KeyPEM.
StorePushCert(ctx context.Context, topic string, certPEM, keyPEM []byte, at time.Time) (PushCert, error)
// PushCert returns the certificate and key for topic, or ErrNotFound.
PushCert(ctx context.Context, topic string) (*PushCert, error)
// PushCerts lists every stored certificate by topic, without keys.
PushCerts(ctx context.Context) ([]PushCert, error)
// PushCertVersion returns the current Version for topic, or ErrNotFound.
PushCertVersion(ctx context.Context, topic string) (int64, error)
}
PushCertStore keeps push certificates and their private keys.
type PushStore ¶
type PushStore interface {
// PushInfo returns push details for the enabled enrollments among ids.
PushInfo(ctx context.Context, ids []mdm.EnrollmentID) (map[mdm.EnrollmentID]mdm.Push, error)
}
PushStore returns what the push layer needs.
type QueuedCommand ¶
type QueuedCommand struct {
Command mdm.Command
State State
DedupeKey string
EnqueuedAt time.Time
LastSentAt time.Time
NotNowUntil time.Time
// Attempts counts deliveries; NotNowCount counts NotNow answers and
// drives the backoff.
Attempts int
NotNowCount int
CompletedAt time.Time
Result *mdm.Response
}
QueuedCommand is a command with its delivery state for one enrollment.
type Result ¶ added in v1.0.0
Result is one page of items with the cursor for the next page ("" at the end).
type State ¶
type State string
State of a queued command.
const ( StatePending State = "pending" // never delivered StateSent State = "sent" // delivered, awaiting a result StateNotNow State = "not-now" // device answered NotNow; retry after NotNowUntil StateAcknowledged State = "acknowledged" // terminal StateError State = "error" // terminal: Error or CommandFormatError StateCleared State = "cleared" // terminal: removed by Clear )
Command states.
type Store ¶
type Store interface {
EnrollmentStore
CommandQueue
PushStore
CertAuthStore
BootstrapTokenStore
PushCertStore
UserAuthStore
MigrationStore
}
Store is everything the service layer needs from one backend.
type UserAuthState ¶
type UserAuthState struct {
ID mdm.EnrollmentID
// Challenge is the outstanding DigestChallenge, "" once answered or
// cleared.
Challenge string
ChallengeAt time.Time
// AuthToken is the issued token, "" until the digest was accepted.
AuthToken string
TokenAt time.Time
// AuthenticateRaw is the first UserAuthenticate plist; DigestRaw the
// second one carrying DigestResponse.
AuthenticateRaw []byte
DigestRaw []byte
}
UserAuthState is the UserAuthenticate handshake state of one user channel (decision record 0016). The user's own enrollment row may not exist yet: the handshake precedes the user channel's TokenUpdate.
type UserAuthStore ¶
type UserAuthStore interface {
// StoreUserAuthChallenge records a new challenge and clears any token.
StoreUserAuthChallenge(ctx context.Context, id mdm.EnrollmentID, challenge string, raw []byte, at time.Time) error
// StoreUserAuthToken records the issued token and clears the challenge.
// ErrNotFound when no challenge was issued for the user.
StoreUserAuthToken(ctx context.Context, id mdm.EnrollmentID, token string, raw []byte, at time.Time) error
// UserAuth returns the state or ErrNotFound.
UserAuth(ctx context.Context, id mdm.EnrollmentID) (*UserAuthState, error)
// ClearUserAuth removes the state; absent state is not an error.
ClearUserAuth(ctx context.Context, id mdm.EnrollmentID) error
}
UserAuthStore persists UserAuthenticate challenges and tokens per user channel. Every method returns ErrInvalid for a device channel and ErrNotFound when the parent device enrollment does not exist. The state is removed when the device re-enrolls.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package crypt seals the per-device secrets a storage backend must retain on Apple's behalf with AES-256-GCM under a named key from a secrets.Provider.
|
Package crypt seals the per-device secrets a storage backend must retain on Apple's behalf with AES-256-GCM under a named key from a secrets.Provider. |
|
Package inmem is the reference storage backend: a mutex-protected map store that every unit test uses.
|
Package inmem is the reference storage backend: a mutex-protected map store that every unit test uses. |
|
Package mysql is the MySQL storage backend on go-sql-driver/mysql.
|
Package mysql is the MySQL storage backend on go-sql-driver/mysql. |
|
Package postgres is the PostgreSQL storage backend on pgx in database/sql mode.
|
Package postgres is the PostgreSQL storage backend on pgx in database/sql mode. |
|
Package sqlcommon implements storage.Store over database/sql once, for every SQL backend: a backend supplies a Dialect (placeholder style, row locking, upsert syntax, and its migration files) and an opened *sql.DB.
|
Package sqlcommon implements storage.Store over database/sql once, for every SQL backend: a backend supplies a Dialect (placeholder style, row locking, upsert syntax, and its migration files) and an opened *sql.DB. |
|
sqltest
Package sqltest holds helpers for SQL backend tests and benchmarks that need large fixtures written faster than the storage API allows.
|
Package sqltest holds helpers for SQL backend tests and benchmarks that need large fixtures written faster than the storage API allows. |
|
Package sqlite is the SQLite storage backend on modernc.org/sqlite (pure Go, no cgo).
|
Package sqlite is the SQLite storage backend on modernc.org/sqlite (pure Go, no cgo). |
|
Package storagetest is the contract every storage backend must satisfy: suites a backend's own test runs through RunAll with a constructor returning a fresh, empty store, and a Failing wrapper that injects errors by method name.
|
Package storagetest is the contract every storage backend must satisfy: suites a backend's own test runs through RunAll with a constructor returning a fresh, empty store, and a Failing wrapper that injects errors by method name. |