fleet/server/datastore/mysql/nanomdm_storage.go
Martin Angers 9082438580
Feature: Remote Lock for macOS, Windows and Linux (#16783)
Feature branch for the #9949  story.

---------

Co-authored-by: Jahziel Villasana-Espinoza <jahziel@fleetdm.com>
Co-authored-by: Roberto Dip <me@roperzh.com>
Co-authored-by: Gabriel Hernandez <ghernandez345@gmail.com>
Co-authored-by: Sarah Gillespie <sarah@fleetdm.com>
2024-02-13 13:03:53 -05:00

184 lines
6 KiB
Go

package mysql
import (
"context"
"crypto/tls"
"errors"
"strings"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"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"
nanodep_client "github.com/micromdm/nanodep/client"
nanodep_mysql "github.com/micromdm/nanodep/storage/mysql"
)
// NanoMDMStorage wraps a *nanomdm_mysql.MySQLStorage and overrides further functionality.
type NanoMDMStorage struct {
*nanomdm_mysql.MySQLStorage
db *sqlx.DB
logger log.Logger
pushCertPEM []byte
pushKeyPEM []byte
}
// NewMDMAppleMDMStorage returns a MySQL nanomdm storage that uses the Datastore
// underlying MySQL writer *sql.DB.
func (ds *Datastore) NewMDMAppleMDMStorage(pushCertPEM []byte, pushKeyPEM []byte) (*NanoMDMStorage, error) {
s, err := nanomdm_mysql.New(nanomdm_mysql.WithDB(ds.primary.DB))
if err != nil {
return nil, err
}
return &NanoMDMStorage{
MySQLStorage: s,
pushCertPEM: pushCertPEM,
pushKeyPEM: pushKeyPEM,
db: ds.primary,
logger: ds.logger,
}, nil
}
// RetrievePushCert partially implements nanomdm_storage.PushCertStore.
//
// Always returns "0" as stale token because we are not storing the APNS in MySQL storage,
// and instead loading them at startup, thus the APNS will never be considered stale.
func (s *NanoMDMStorage) RetrievePushCert(
ctx context.Context, topic string,
) (cert *tls.Certificate, staleToken string, err error) {
tlsCert, err := tls.X509KeyPair(s.pushCertPEM, s.pushKeyPEM)
if err != nil {
return nil, "", err
}
return &tlsCert, "0", nil
}
// IsPushCertStale partially implements nanomdm_storage.PushCertStore.
//
// Given that we are not storing the APNS certificate in MySQL storage, and instead loading
// them at startup (as env variables), the APNS will never be considered stale.
//
// TODO(lucas): Revisit solution to support changing the APNS.
func (s *NanoMDMStorage) IsPushCertStale(ctx context.Context, topic, staleToken string) (bool, error) {
return false, nil
}
// StorePushCert partially implements nanomdm_storage.PushCertStore.
//
// Leaving this unimplemented as APNS certificate and key are not stored in MySQL storage,
// instead they are loaded to memory at startup.
func (s *NanoMDMStorage) StorePushCert(ctx context.Context, pemCert, pemKey []byte) error {
return errors.New("unimplemented")
}
// 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
}
// TODO(roberto): call @mna's transactionable method to update
// these tables when it's ready.
stmt := `
INSERT INTO host_mdm_actions (
host_id,
lock_ref,
unlock_pin
)
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); err != nil {
return ctxerr.Wrap(ctx, err, "modifying host_mdm_actions for DeviceLock")
}
return nil
}, s.logger)
}
// NewMDMAppleDEPStorage returns a MySQL nanodep storage that uses the Datastore
// underlying MySQL writer *sql.DB.
func (ds *Datastore) NewMDMAppleDEPStorage(tok nanodep_client.OAuth1Tokens) (*NanoDEPStorage, error) {
s, err := nanodep_mysql.New(nanodep_mysql.WithDB(ds.primary.DB))
if err != nil {
return nil, err
}
return &NanoDEPStorage{
MySQLStorage: s,
tokens: tok,
}, nil
}
// NanoDEPStorage wraps a *nanodep_mysql.MySQLStorage and overrides functionality to load
// DEP auth tokens from memory.
type NanoDEPStorage struct {
*nanodep_mysql.MySQLStorage
tokens nanodep_client.OAuth1Tokens
}
// RetrieveAuthTokens partially implements nanodep.AuthTokensRetriever.
//
// RetrieveAuthTokens returns the DEP auth tokens stored in memory.
func (s *NanoDEPStorage) RetrieveAuthTokens(ctx context.Context, name string) (*nanodep_client.OAuth1Tokens, error) {
return &s.tokens, nil
}
// StoreAuthTokens partially implements nanodep.AuthTokensStorer.
//
// Leaving this unimplemented as DEP auth tokens are not stored in MySQL storage,
// instead they are loaded to memory at startup.
func (s *NanoDEPStorage) StoreAuthTokens(ctx context.Context, name string, tokens *nanodep_client.OAuth1Tokens) error {
return errors.New("unimplemented")
}
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
}