fleet/server/datastore/mysql/nanomdm_storage.go
Roberto Dip 545e56d288
19016 ingest certs on start (#19360)
For #19016

This changes all the places where we previously assumed that certs were
hardcoded when the Fleet server started to query the database instead.

The plan is to loadtest afterwards, but as a first preemptive measure,
this adds a caching layer on top the mysql datastore.

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Added/updated tests
- [x] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
- [x] Manual QA for all new/changed functionality
2024-05-30 18:18:42 -03:00

203 lines
6.3 KiB
Go

package mysql
import (
"context"
"crypto/tls"
"errors"
"fmt"
"strings"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/assets"
nanodep_client "github.com/fleetdm/fleet/v4/server/mdm/nanodep/client"
nanodep_mysql "github.com/fleetdm/fleet/v4/server/mdm/nanodep/storage/mysql"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
nanomdm_mysql "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/storage/mysql"
"github.com/go-kit/log"
"github.com/jmoiron/sqlx"
)
// NanoMDMStorage wraps a *nanomdm_mysql.MySQLStorage and overrides further functionality.
type NanoMDMStorage struct {
*nanomdm_mysql.MySQLStorage
db *sqlx.DB
logger log.Logger
ds fleet.Datastore
}
// NewMDMAppleMDMStorage returns a MySQL nanomdm storage that uses the Datastore
// underlying MySQL writer *sql.DB.
func (ds *Datastore) NewMDMAppleMDMStorage() (*NanoMDMStorage, error) {
s, err := nanomdm_mysql.New(nanomdm_mysql.WithDB(ds.primary.DB))
if err != nil {
return nil, err
}
return &NanoMDMStorage{
MySQLStorage: s,
db: ds.primary,
logger: ds.logger,
ds: ds,
}, nil
}
// RetrievePushCert partially implements nanomdm_storage.PushCertStore.
//
// Always returns "0" as stale token because fleet.Datastore always returns a valid push certificate.
func (s *NanoMDMStorage) RetrievePushCert(
ctx context.Context, topic string,
) (*tls.Certificate, string, error) {
cert, err := assets.APNSKeyPair(ctx, s.ds)
if err != nil {
return nil, "", ctxerr.Wrap(ctx, err, "loading push certificate")
}
return cert, "0", nil
}
// IsPushCertStale partially implements nanomdm_storage.PushCertStore.
//
// Always returns `false` because the underlying datastore implementation makes sure that the token is always fresh.
func (s *NanoMDMStorage) IsPushCertStale(ctx context.Context, topic, staleToken string) (bool, error) {
return false, nil
}
// StorePushCert partially implements nanomdm_storage.PushCertStore.
func (s *NanoMDMStorage) StorePushCert(ctx context.Context, pemCert, pemKey []byte) error {
return errors.New("please use fleet.Datastore to manage MDM assets")
}
// EnqueueDeviceLockCommand enqueues a DeviceLock command for the given host.
//
// A few implementation details:
// - It can only be called for a single hosts, to ensure we don't use the same
// pin for multiple hosts.
// - The method performs fleet-specific actions after the command is enqueued.
func (s *NanoMDMStorage) EnqueueDeviceLockCommand(
ctx context.Context,
host *fleet.Host,
cmd *mdm.Command,
pin string,
) error {
return withRetryTxx(ctx, s.db, func(tx sqlx.ExtContext) error {
if err := enqueueCommandDB(ctx, tx, []string{host.UUID}, cmd); err != nil {
return err
}
stmt := `
INSERT INTO host_mdm_actions (
host_id,
lock_ref,
unlock_pin,
fleet_platform
)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
wipe_ref = NULL,
unlock_ref = NULL,
unlock_pin = VALUES(unlock_pin),
lock_ref = VALUES(lock_ref)`
if _, err := tx.ExecContext(ctx, stmt, host.ID, cmd.CommandUUID, pin, host.FleetPlatform()); err != nil {
return ctxerr.Wrap(ctx, err, "modifying host_mdm_actions for DeviceLock")
}
return nil
}, s.logger)
}
// EnqueueDeviceWipeCommand enqueues a EraseDevice command for the given host.
func (s *NanoMDMStorage) EnqueueDeviceWipeCommand(ctx context.Context, host *fleet.Host, cmd *mdm.Command) error {
return withRetryTxx(ctx, s.db, func(tx sqlx.ExtContext) error {
if err := enqueueCommandDB(ctx, tx, []string{host.UUID}, cmd); err != nil {
return err
}
stmt := `
INSERT INTO host_mdm_actions (
host_id,
wipe_ref,
fleet_platform
)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE
wipe_ref = VALUES(wipe_ref)`
if _, err := tx.ExecContext(ctx, stmt, host.ID, cmd.CommandUUID, host.FleetPlatform()); err != nil {
return ctxerr.Wrap(ctx, err, "modifying host_mdm_actions for DeviceWipe")
}
return nil
}, s.logger)
}
func (s *NanoMDMStorage) GetAllMDMConfigAssetsByName(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
return s.ds.GetAllMDMConfigAssetsByName(ctx, assetNames)
}
// NewMDMAppleDEPStorage returns a MySQL nanodep storage that uses the Datastore
// underlying MySQL writer *sql.DB.
func (ds *Datastore) NewMDMAppleDEPStorage() (*NanoDEPStorage, error) {
s, err := nanodep_mysql.New(nanodep_mysql.WithDB(ds.primary.DB))
if err != nil {
return nil, err
}
return &NanoDEPStorage{
MySQLStorage: s,
ds: ds,
}, nil
}
// NanoDEPStorage wraps a *nanodep_mysql.MySQLStorage and overrides functionality to load
// DEP auth tokens from the tables managed by Fleet.
type NanoDEPStorage struct {
*nanodep_mysql.MySQLStorage
ds fleet.Datastore
}
// RetrieveAuthTokens partially implements nanodep.AuthTokensRetriever.
func (s *NanoDEPStorage) RetrieveAuthTokens(ctx context.Context, name string) (*nanodep_client.OAuth1Tokens, error) {
token, err := assets.ABMToken(ctx, s.ds)
if err != nil {
return nil, fmt.Errorf("retrieving token in nano dep storage: %w", err)
}
return token, nil
}
// StoreAuthTokens partially implements nanodep.AuthTokensStorer.
func (s *NanoDEPStorage) StoreAuthTokens(ctx context.Context, name string, tokens *nanodep_client.OAuth1Tokens) error {
return errors.New("please use fleet.Datastore to manage MDM assets")
}
func enqueueCommandDB(ctx context.Context, tx sqlx.ExtContext, ids []string, cmd *mdm.Command) error {
// NOTE: the code to insert into nano_commands and
// nano_enrollment_queue was copied verbatim from the nanomdm
// implementation. Ideally we modify some of the interfaces to not
// duplicate the code here, but that needs more careful planning
// (which we lack right now)
if len(ids) < 1 {
return errors.New("no id(s) supplied to queue command to")
}
_, err := tx.ExecContext(
ctx,
`INSERT INTO nano_commands (command_uuid, request_type, command) VALUES (?, ?, ?);`,
cmd.CommandUUID, cmd.Command.RequestType, cmd.Raw,
)
if err != nil {
return err
}
query := `INSERT INTO nano_enrollment_queue (id, command_uuid) VALUES (?, ?)`
query += strings.Repeat(", (?, ?)", len(ids)-1)
args := make([]interface{}, len(ids)*2)
for i, id := range ids {
args[i*2] = id
args[i*2+1] = cmd.CommandUUID
}
if _, err = tx.ExecContext(ctx, query+";", args...); err != nil {
return err
}
return nil
}