mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Apply custom setup assistants (if present) when ingesting new devices (#11563)
This commit is contained in:
parent
487f8b6e1f
commit
70f18dda4a
18 changed files with 423 additions and 327 deletions
|
|
@ -0,0 +1 @@
|
|||
* Integrated the macOS setup assistant feature with Apple DEP so that the setup assistants are assigned to the enrolled devices.
|
||||
|
|
@ -21,7 +21,6 @@ import (
|
|||
kitlog "github.com/go-kit/kit/log"
|
||||
"github.com/go-kit/kit/log/level"
|
||||
"github.com/google/uuid"
|
||||
"github.com/micromdm/nanodep/godep"
|
||||
"github.com/micromdm/nanodep/storage"
|
||||
)
|
||||
|
||||
|
|
@ -387,7 +386,7 @@ func (svc *Service) SetOrUpdateMDMAppleSetupAssistant(ctx context.Context, asst
|
|||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("profile", msg))
|
||||
}
|
||||
}
|
||||
// TODO(mna): svc.depService.RegisterProfileWithAppleDEPServer()
|
||||
// TODO(mna): enqueue job to Define/Assign profile svc.depService.RegisterProfileWithAppleDEPServer()
|
||||
|
||||
// must read the existing setup assistant first to detect if it did change
|
||||
// (so that the changed activity is not created if the same assistant was
|
||||
|
|
@ -543,6 +542,7 @@ func (svc *Service) InitiateMDMAppleSSOCallback(ctx context.Context, auth fleet.
|
|||
return "", ctxerr.Wrap(ctx, err, "getting EULA metadata")
|
||||
}
|
||||
|
||||
// get the automatic profile to access the authentication token.
|
||||
depProf, err := svc.getAutomaticEnrollmentProfile(ctx)
|
||||
if err != nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "listing profiles")
|
||||
|
|
@ -560,53 +560,30 @@ func (svc *Service) InitiateMDMAppleSSOCallback(ctx context.Context, auth fleet.
|
|||
return appConfig.ServerSettings.ServerURL + "/mdm/sso/callback?" + q.Encode(), nil
|
||||
}
|
||||
|
||||
func (svc *Service) mdmAppleSyncDEPProfile(ctx context.Context) error {
|
||||
func (svc *Service) mdmAppleSyncDEPProfiles(ctx context.Context) error {
|
||||
// TODO(mna): all profiles must be updated: this gets called when the ServerURL or MDM
|
||||
// SSO got modified. And then all devices part of the profile's team must be re-assigned
|
||||
// the updated profile. Enqueue a worker job to take care of this.
|
||||
|
||||
// get the automatic enrollment profile to re-define it with Apple.
|
||||
depProf, err := svc.getAutomaticEnrollmentProfile(ctx)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "fetching enrollment profile")
|
||||
}
|
||||
|
||||
if depProf == nil {
|
||||
// CreateDefaultProfile takes care of registering the profile with Apple.
|
||||
return svc.depService.CreateDefaultProfile(ctx)
|
||||
}
|
||||
|
||||
appCfg, err := svc.ds.AppConfig(ctx)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "fetching app config")
|
||||
}
|
||||
|
||||
enrollURL, err := apple_mdm.EnrollURL(depProf.Token, appCfg)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "generating enroll URL")
|
||||
}
|
||||
|
||||
var jsonProf *godep.Profile
|
||||
if err := json.Unmarshal(*depProf.DEPProfile, &jsonProf); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "unmarshalling DEP profile")
|
||||
}
|
||||
|
||||
return svc.depService.RegisterProfileWithAppleDEPServer(ctx, jsonProf, enrollURL)
|
||||
return svc.depService.RegisterProfileWithAppleDEPServer(ctx, nil)
|
||||
}
|
||||
|
||||
// returns the default automatic enrollment profile, or nil (without error) if none exists.
|
||||
func (svc *Service) getAutomaticEnrollmentProfile(ctx context.Context) (*fleet.MDMAppleEnrollmentProfile, error) {
|
||||
profiles, err := svc.ds.ListMDMAppleEnrollmentProfiles(ctx)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "listing profiles")
|
||||
prof, err := svc.ds.GetMDMAppleEnrollmentProfileByType(ctx, fleet.MDMAppleEnrollmentTypeAutomatic)
|
||||
if err != nil && !fleet.IsNotFound(err) {
|
||||
return nil, ctxerr.Wrap(ctx, err, "get automatic profile")
|
||||
}
|
||||
|
||||
// Grab the first automatic enrollment profile we find, the current
|
||||
// behavior is that the last enrollment profile that was uploaded is
|
||||
// the one assigned to newly enrolled devices.
|
||||
//
|
||||
// TODO: this will change after #10995 where there can be a DEP profile
|
||||
// per team.
|
||||
var depProf *fleet.MDMAppleEnrollmentProfile
|
||||
for _, prof := range profiles {
|
||||
if prof.Type == "automatic" {
|
||||
depProf = prof
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return depProf, nil
|
||||
return prof, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ func NewService(
|
|||
MDMAppleEnableFileVaultAndEscrow: eeservice.MDMAppleEnableFileVaultAndEscrow,
|
||||
MDMAppleDisableFileVaultAndEscrow: eeservice.MDMAppleDisableFileVaultAndEscrow,
|
||||
DeleteMDMAppleSetupAssistant: eeservice.DeleteMDMAppleSetupAssistant,
|
||||
MDMAppleSyncDEPProfile: eeservice.mdmAppleSyncDEPProfile,
|
||||
MDMAppleSyncDEPProfiles: eeservice.mdmAppleSyncDEPProfiles,
|
||||
DeleteMDMAppleBootstrapPackage: eeservice.DeleteMDMAppleBootstrapPackage,
|
||||
})
|
||||
|
||||
|
|
|
|||
28
mdm_profiles/setup_assistant.json
Normal file
28
mdm_profiles/setup_assistant.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"profile_name": "FleetDM example enrollment profile",
|
||||
"allow_pairing": true,
|
||||
"is_mdm_removable": true,
|
||||
"org_magic": "1",
|
||||
"language": "en",
|
||||
"region": "US",
|
||||
"skip_setup_items": [
|
||||
"Accessibility",
|
||||
"Appearance",
|
||||
"AppleID",
|
||||
"AppStore",
|
||||
"Biometric",
|
||||
"Diagnostics",
|
||||
"FileVault",
|
||||
"iCloudDiagnostics",
|
||||
"iCloudStorage",
|
||||
"Location",
|
||||
"Payment",
|
||||
"Privacy",
|
||||
"Restore",
|
||||
"ScreenTime",
|
||||
"Siri",
|
||||
"TermsOfAddress",
|
||||
"TOS",
|
||||
"UnlockWithWatch"
|
||||
]
|
||||
}
|
||||
|
|
@ -252,7 +252,7 @@ ORDER BY created_at DESC
|
|||
|
||||
func (ds *Datastore) GetMDMAppleEnrollmentProfileByToken(ctx context.Context, token string) (*fleet.MDMAppleEnrollmentProfile, error) {
|
||||
var enrollment fleet.MDMAppleEnrollmentProfile
|
||||
if err := sqlx.GetContext(ctx, ds.writer,
|
||||
if err := sqlx.GetContext(ctx, ds.reader,
|
||||
&enrollment,
|
||||
`
|
||||
SELECT
|
||||
|
|
@ -277,6 +277,33 @@ WHERE
|
|||
return &enrollment, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) GetMDMAppleEnrollmentProfileByType(ctx context.Context, typ fleet.MDMAppleEnrollmentType) (*fleet.MDMAppleEnrollmentProfile, error) {
|
||||
var enrollment fleet.MDMAppleEnrollmentProfile
|
||||
if err := sqlx.GetContext(ctx, ds.writer, // use writer as it is used just after creation in some cases
|
||||
&enrollment,
|
||||
`
|
||||
SELECT
|
||||
id,
|
||||
token,
|
||||
type,
|
||||
dep_profile,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
mdm_apple_enrollment_profiles
|
||||
WHERE
|
||||
type = ?
|
||||
`,
|
||||
typ,
|
||||
); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, ctxerr.Wrap(ctx, notFound("MDMAppleEnrollmentProfile"))
|
||||
}
|
||||
return nil, ctxerr.Wrap(ctx, err, "get enrollment profile by type")
|
||||
}
|
||||
return &enrollment, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) GetMDMAppleCommandRequestType(ctx context.Context, commandUUID string) (string, error) {
|
||||
var rt string
|
||||
err := sqlx.GetContext(ctx, ds.reader, &rt, `SELECT request_type FROM nano_commands WHERE command_uuid = ?`, commandUUID)
|
||||
|
|
@ -599,20 +626,20 @@ func insertMDMAppleHostDB(
|
|||
return nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) IngestMDMAppleDevicesFromDEPSync(ctx context.Context, devices []godep.Device) (int64, error) {
|
||||
func (ds *Datastore) IngestMDMAppleDevicesFromDEPSync(ctx context.Context, devices []godep.Device) (createdCount int64, teamID *uint, err error) {
|
||||
if len(devices) < 1 {
|
||||
level.Debug(ds.logger).Log("msg", "ingesting devices from DEP received < 1 device, skipping", "len(devices)", len(devices))
|
||||
return 0, nil
|
||||
return 0, nil, nil
|
||||
}
|
||||
filteredDevices := filterMDMAppleDevices(devices, ds.logger)
|
||||
if len(filteredDevices) < 1 {
|
||||
level.Debug(ds.logger).Log("msg", "ingesting devices from DEP filtered all devices, skipping", "len(devices)", len(devices))
|
||||
return 0, nil
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
appCfg, err := ds.AppConfig(ctx)
|
||||
if err != nil {
|
||||
return 0, ctxerr.Wrap(ctx, err, "ingest mdm apple host get app config")
|
||||
return 0, nil, ctxerr.Wrap(ctx, err, "ingest mdm apple host get app config")
|
||||
}
|
||||
|
||||
args := []interface{}{nil}
|
||||
|
|
@ -629,13 +656,13 @@ func (ds *Datastore) IngestMDMAppleDevicesFromDEPSync(ctx context.Context, devic
|
|||
// If the team doesn't exist, we still ingest the device, but it won't
|
||||
// belong to any team.
|
||||
case err != nil:
|
||||
return 0, ctxerr.Wrap(ctx, err, "ingest mdm apple host get team by name")
|
||||
return 0, nil, ctxerr.Wrap(ctx, err, "ingest mdm apple host get team by name")
|
||||
default:
|
||||
args[0] = team.ID
|
||||
teamID = &team.ID
|
||||
}
|
||||
}
|
||||
|
||||
var resCount int64
|
||||
err = ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||
us, unionArgs := unionSelectDevices(filteredDevices)
|
||||
args = append(args, unionArgs...)
|
||||
|
|
@ -678,7 +705,7 @@ func (ds *Datastore) IngestMDMAppleDevicesFromDEPSync(ctx context.Context, devic
|
|||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "ingest mdm apple hosts from dep sync rows affected")
|
||||
}
|
||||
resCount = n
|
||||
createdCount = n
|
||||
|
||||
// get new host ids
|
||||
args = []interface{}{}
|
||||
|
|
@ -715,7 +742,7 @@ func (ds *Datastore) IngestMDMAppleDevicesFromDEPSync(ctx context.Context, devic
|
|||
return nil
|
||||
})
|
||||
|
||||
return resCount, err
|
||||
return createdCount, teamID, err
|
||||
}
|
||||
|
||||
func upsertMDMAppleHostDisplayNamesDB(ctx context.Context, tx sqlx.ExtContext, hosts ...fleet.Host) error {
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ func TestMDMApple(t *testing.T) {
|
|||
{"TestListMDMAppleCommands", testListMDMAppleCommands},
|
||||
{"TestMDMAppleEULA", testMDMAppleEULA},
|
||||
{"TestMDMAppleSetupAssistant", testMDMAppleSetupAssistant},
|
||||
{"TestMDMAppleEnrollmentProfile", testMDMAppleEnrollmentProfile},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
|
|
@ -443,9 +444,10 @@ func TestIngestMDMAppleDevicesFromDEPSync(t *testing.T) {
|
|||
}
|
||||
wantSerials = append(wantSerials, "abc", "xyz", "ijk")
|
||||
|
||||
n, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, depDevices)
|
||||
n, tmID, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, depDevices)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(3), n) // 3 new hosts ("abc", "xyz", "ijk")
|
||||
require.Nil(t, tmID)
|
||||
|
||||
hosts = listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, len(wantSerials))
|
||||
gotSerials := []string{}
|
||||
|
|
@ -468,8 +470,9 @@ func TestDEPSyncTeamAssignment(t *testing.T) {
|
|||
{SerialNumber: "def", Model: "MacBook Pro", OS: "OSX", OpType: "added"},
|
||||
}
|
||||
|
||||
n, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, depDevices)
|
||||
n, tmID, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, depDevices)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, tmID)
|
||||
require.Equal(t, int64(2), n)
|
||||
|
||||
hosts := listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, 2)
|
||||
|
|
@ -493,9 +496,11 @@ func TestDEPSyncTeamAssignment(t *testing.T) {
|
|||
{SerialNumber: "xyz", Model: "MacBook Pro", OS: "OSX", OpType: "added"},
|
||||
}
|
||||
|
||||
n, err = ds.IngestMDMAppleDevicesFromDEPSync(ctx, depDevices)
|
||||
n, tmID, err = ds.IngestMDMAppleDevicesFromDEPSync(ctx, depDevices)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(1), n)
|
||||
require.NotNil(t, tmID)
|
||||
require.Equal(t, team.ID, *tmID)
|
||||
|
||||
hosts = listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, 3)
|
||||
for _, h := range hosts {
|
||||
|
|
@ -514,9 +519,10 @@ func TestDEPSyncTeamAssignment(t *testing.T) {
|
|||
{SerialNumber: "jqk", Model: "MacBook Pro", OS: "OSX", OpType: "added"},
|
||||
}
|
||||
|
||||
n, err = ds.IngestMDMAppleDevicesFromDEPSync(ctx, depDevices)
|
||||
n, tmID, err = ds.IngestMDMAppleDevicesFromDEPSync(ctx, depDevices)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, n, 1)
|
||||
require.Nil(t, tmID)
|
||||
|
||||
hosts = listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, 4)
|
||||
for _, h := range hosts {
|
||||
|
|
@ -638,11 +644,12 @@ func testIngestMDMAppleIngestAfterDEPSync(t *testing.T, ds *Datastore) {
|
|||
testModel := "MacBook Pro"
|
||||
|
||||
// simulate a host that is first ingested via DEP (e.g., the device was added via Apple Business Manager)
|
||||
n, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, []godep.Device{
|
||||
n, tmID, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, []godep.Device{
|
||||
{SerialNumber: testSerial, Model: testModel, OS: "OSX", OpType: "added"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(1), n)
|
||||
require.Nil(t, tmID)
|
||||
|
||||
hosts := listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, 1)
|
||||
// hosts that are first ingested via DEP will have a serial number but not a UUID because UUID
|
||||
|
|
@ -684,11 +691,12 @@ func testIngestMDMAppleCheckinBeforeDEPSync(t *testing.T, ds *Datastore) {
|
|||
checkMDMHostRelatedTables(t, ds, hosts[0].ID, testSerial, testModel)
|
||||
|
||||
// no effect if same host appears in DEP sync
|
||||
n, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, []godep.Device{
|
||||
n, tmID, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, []godep.Device{
|
||||
{SerialNumber: testSerial, Model: testModel, OS: "OSX", OpType: "added"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(0), n)
|
||||
require.Nil(t, tmID)
|
||||
|
||||
hosts = listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, 1)
|
||||
require.Equal(t, testSerial, hosts[0].HardwareSerial)
|
||||
|
|
@ -3407,3 +3415,56 @@ func testMDMAppleSetupAssistant(t *testing.T, ds *Datastore) {
|
|||
err = ds.DeleteMDMAppleSetupAssistant(ctx, &tm.ID)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func testMDMAppleEnrollmentProfile(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := ds.GetMDMAppleEnrollmentProfileByType(ctx, fleet.MDMAppleEnrollmentTypeAutomatic)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||
|
||||
_, err = ds.GetMDMAppleEnrollmentProfileByToken(ctx, "abcd")
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||
|
||||
// add a new automatic enrollment profile
|
||||
rawMsg := json.RawMessage(`{"allow_pairing": true}`)
|
||||
profAuto, err := ds.NewMDMAppleEnrollmentProfile(ctx, fleet.MDMAppleEnrollmentProfilePayload{
|
||||
Type: "automatic",
|
||||
DEPProfile: &rawMsg,
|
||||
Token: "abcd",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, profAuto.ID)
|
||||
|
||||
// add a new manual enrollment profile
|
||||
profMan, err := ds.NewMDMAppleEnrollmentProfile(ctx, fleet.MDMAppleEnrollmentProfilePayload{
|
||||
Type: "manual",
|
||||
DEPProfile: &rawMsg,
|
||||
Token: "efgh",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, profMan.ID)
|
||||
|
||||
profs, err := ds.ListMDMAppleEnrollmentProfiles(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, profs, 2)
|
||||
|
||||
tokens := make([]string, 2)
|
||||
for i, p := range profs {
|
||||
tokens[i] = p.Token
|
||||
}
|
||||
require.ElementsMatch(t, []string{"abcd", "efgh"}, tokens)
|
||||
|
||||
// get the automatic profile by type
|
||||
getProf, err := ds.GetMDMAppleEnrollmentProfileByType(ctx, fleet.MDMAppleEnrollmentTypeAutomatic)
|
||||
require.NoError(t, err)
|
||||
getProf.UpdateCreateTimestamps = fleet.UpdateCreateTimestamps{}
|
||||
require.Equal(t, profAuto, getProf)
|
||||
|
||||
// get the manual profile by token
|
||||
getProf, err = ds.GetMDMAppleEnrollmentProfileByToken(ctx, "efgh")
|
||||
require.NoError(t, err)
|
||||
getProf.UpdateCreateTimestamps = fleet.UpdateCreateTimestamps{}
|
||||
require.Equal(t, profMan, getProf)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1098,11 +1098,12 @@ func testHostsListMDM(t *testing.T, ds *Datastore) {
|
|||
}
|
||||
|
||||
// enrollment: pending (with Fleet mdm)
|
||||
n, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, []godep.Device{
|
||||
n, tmID, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, []godep.Device{
|
||||
{SerialNumber: "532141num832", Model: "MacBook Pro", OS: "OSX", OpType: "added"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(1), n)
|
||||
require.Nil(t, tmID)
|
||||
|
||||
const simpleMDM, kandji, unknown = "https://simplemdm.com", "https://kandji.io", "https://url.com"
|
||||
err = ds.SetOrUpdateMDMData(ctx, hostIDs[0], false, true, simpleMDM, true, fleet.WellKnownMDMSimpleMDM) // enrollment: automatic
|
||||
|
|
|
|||
|
|
@ -763,11 +763,11 @@ type Datastore interface {
|
|||
NewMDMAppleEnrollmentProfile(ctx context.Context, enrollmentPayload MDMAppleEnrollmentProfilePayload) (*MDMAppleEnrollmentProfile, error)
|
||||
|
||||
// GetMDMAppleEnrollmentProfileByToken loads the enrollment profile from its secret token.
|
||||
// TODO(mna): this may have to be removed if we don't end up supporting
|
||||
// manual enrollment via a token (currently we only support it via Fleet
|
||||
// Desktop, in the My Device page). See #8701.
|
||||
GetMDMAppleEnrollmentProfileByToken(ctx context.Context, token string) (*MDMAppleEnrollmentProfile, error)
|
||||
|
||||
// GetMDMAppleEnrollmentProfileByType loads the enrollment profile from its type (e.g. manual, automatic).
|
||||
GetMDMAppleEnrollmentProfileByType(ctx context.Context, typ MDMAppleEnrollmentType) (*MDMAppleEnrollmentProfile, error)
|
||||
|
||||
// ListMDMAppleEnrollmentProfiles returns the list of all the enrollment profiles.
|
||||
ListMDMAppleEnrollmentProfiles(ctx context.Context) ([]*MDMAppleEnrollmentProfile, error)
|
||||
|
||||
|
|
@ -806,8 +806,9 @@ type Datastore interface {
|
|||
MDMAppleListDevices(ctx context.Context) ([]MDMAppleDevice, error)
|
||||
|
||||
// IngestMDMAppleDevicesFromDEPSync creates new Fleet host records for MDM-enrolled devices that are
|
||||
// not already enrolled in Fleet.
|
||||
IngestMDMAppleDevicesFromDEPSync(ctx context.Context, devices []godep.Device) (int64, error)
|
||||
// not already enrolled in Fleet. It returns the number of hosts created, the team id that they
|
||||
// joined (nil for no team), and an error.
|
||||
IngestMDMAppleDevicesFromDEPSync(ctx context.Context, devices []godep.Device) (int64, *uint, error)
|
||||
|
||||
// IngestMDMAppleDeviceFromCheckin creates a new Fleet host record for an MDM-enrolled device that is
|
||||
// not already enrolled in Fleet.
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ type EnterpriseOverrides struct {
|
|||
MDMAppleEnableFileVaultAndEscrow func(ctx context.Context, teamID *uint) error
|
||||
MDMAppleDisableFileVaultAndEscrow func(ctx context.Context, teamID *uint) error
|
||||
DeleteMDMAppleSetupAssistant func(ctx context.Context, teamID *uint) error
|
||||
MDMAppleSyncDEPProfile func(ctx context.Context) error
|
||||
MDMAppleSyncDEPProfiles func(ctx context.Context) error
|
||||
DeleteMDMAppleBootstrapPackage func(ctx context.Context, teamID *uint) error
|
||||
}
|
||||
|
||||
|
|
@ -600,13 +600,6 @@ type Service interface {
|
|||
// to any team).
|
||||
GetMDMAppleFileVaultSummary(ctx context.Context, teamID *uint) (*MDMAppleFileVaultSummary, error)
|
||||
|
||||
// NewMDMAppleEnrollmentProfile creates and returns new enrollment profile.
|
||||
// Such enrollment profiles allow devices to enroll to Fleet MDM.
|
||||
NewMDMAppleEnrollmentProfile(ctx context.Context, enrollmentPayload MDMAppleEnrollmentProfilePayload) (enrollmentProfile *MDMAppleEnrollmentProfile, err error)
|
||||
|
||||
// ListMDMAppleEnrollmentProfiles returns the list of all the enrollment profiles.
|
||||
ListMDMAppleEnrollmentProfiles(ctx context.Context) ([]*MDMAppleEnrollmentProfile, error)
|
||||
|
||||
// GetMDMAppleEnrollmentProfileByToken returns the Apple enrollment from its secret token.
|
||||
// TODO(mna): this may have to be removed if we don't end up supporting
|
||||
// manual enrollment via a token (currently we only support it via Fleet
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ package apple_mdm
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
|
|
@ -87,8 +89,8 @@ type DEPService struct {
|
|||
logger kitlog.Logger
|
||||
}
|
||||
|
||||
// GetDefaultProfile returns a godep.Profile with default values set.
|
||||
func (d *DEPService) GetDefaultProfile() *godep.Profile {
|
||||
// getDefaultProfile returns a godep.Profile with default values set.
|
||||
func (d *DEPService) getDefaultProfile() *godep.Profile {
|
||||
return &godep.Profile{
|
||||
ProfileName: "FleetDM default enrollment profile",
|
||||
AllowPairing: true,
|
||||
|
|
@ -127,7 +129,7 @@ func (d *DEPService) GetDefaultProfile() *godep.Profile {
|
|||
// CreateDefaultProfile creates a new DEP enrollment profile with default
|
||||
// values in the database and registers it in Apple's servers.
|
||||
func (d *DEPService) CreateDefaultProfile(ctx context.Context) error {
|
||||
if err := d.createProfile(ctx, d.GetDefaultProfile()); err != nil {
|
||||
if err := d.createProfile(ctx, d.getDefaultProfile()); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "creating profile")
|
||||
}
|
||||
return nil
|
||||
|
|
@ -140,11 +142,6 @@ func (d *DEPService) CreateDefaultProfile(ctx context.Context) error {
|
|||
// https://developer.apple.com/documentation/devicemanagement/profile
|
||||
func (d *DEPService) createProfile(ctx context.Context, depProfile *godep.Profile) error {
|
||||
token := uuid.New().String()
|
||||
enrollURL, err := d.EnrollURL(token)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "generating enroll URL")
|
||||
}
|
||||
|
||||
rawDEPProfile, err := json.Marshal(depProfile)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "marshaling provided profile")
|
||||
|
|
@ -152,15 +149,14 @@ func (d *DEPService) createProfile(ctx context.Context, depProfile *godep.Profil
|
|||
|
||||
payload := fleet.MDMAppleEnrollmentProfilePayload{
|
||||
Token: token,
|
||||
Type: "automatic",
|
||||
Type: fleet.MDMAppleEnrollmentTypeAutomatic,
|
||||
DEPProfile: ptr.RawMessage(rawDEPProfile),
|
||||
}
|
||||
|
||||
if _, err := d.ds.NewMDMAppleEnrollmentProfile(ctx, payload); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "saving enrollment profile in DB")
|
||||
}
|
||||
|
||||
if err := d.RegisterProfileWithAppleDEPServer(ctx, depProfile, enrollURL); err != nil {
|
||||
if err := d.RegisterProfileWithAppleDEPServer(ctx, nil); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "registering profile in Apple servers")
|
||||
}
|
||||
|
||||
|
|
@ -168,14 +164,40 @@ func (d *DEPService) createProfile(ctx context.Context, depProfile *godep.Profil
|
|||
}
|
||||
|
||||
// RegisterProfileWithAppleDEPServer registers the enrollment profile in
|
||||
// Apple's servers via the DEP API, so it can be used for assignment.
|
||||
func (d *DEPService) RegisterProfileWithAppleDEPServer(ctx context.Context, depProfile *godep.Profile, enrollURL string) error {
|
||||
appConfig, err := d.ds.AppConfig(ctx)
|
||||
// Apple's servers via the DEP API, so it can be used for assignment. If
|
||||
// setupAsst is nil, the default profile is registered.
|
||||
func (d *DEPService) RegisterProfileWithAppleDEPServer(ctx context.Context, setupAsst *fleet.MDMAppleSetupAssistant) error {
|
||||
appCfg, err := d.ds.AppConfig(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get app config: %w", err)
|
||||
return ctxerr.Wrap(ctx, err, "fetching app config")
|
||||
}
|
||||
|
||||
depProfile.URL = enrollURL
|
||||
// must always get the default profile, because the authentication token is
|
||||
// defined on that profile.
|
||||
defaultProf, err := d.ds.GetMDMAppleEnrollmentProfileByType(ctx, fleet.MDMAppleEnrollmentTypeAutomatic)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "fetching default profile")
|
||||
}
|
||||
|
||||
enrollURL, err := EnrollURL(defaultProf.Token, appCfg)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "generating enroll URL")
|
||||
}
|
||||
|
||||
var rawJSON json.RawMessage
|
||||
if defaultProf.DEPProfile != nil {
|
||||
rawJSON = *defaultProf.DEPProfile
|
||||
}
|
||||
if setupAsst != nil {
|
||||
rawJSON = setupAsst.Profile
|
||||
}
|
||||
|
||||
var jsonProf *godep.Profile
|
||||
if err := json.Unmarshal(rawJSON, &jsonProf); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "unmarshalling DEP profile")
|
||||
}
|
||||
|
||||
jsonProf.URL = enrollURL
|
||||
|
||||
// If SSO is configured, use the `/mdm/sso` page which starts the SSO
|
||||
// flow, otherwise use Fleet's enroll URL.
|
||||
|
|
@ -184,65 +206,114 @@ func (d *DEPService) RegisterProfileWithAppleDEPServer(ctx context.Context, depP
|
|||
// always still set configuration_web_url, otherwise the request method
|
||||
// coming from Apple changes from GET to POST, and we want to preserve
|
||||
// backwards compatibility.
|
||||
if appConfig.MDM.EndUserAuthentication.SSOProviderSettings.IsEmpty() {
|
||||
depProfile.ConfigurationWebURL = enrollURL
|
||||
if appCfg.MDM.EndUserAuthentication.SSOProviderSettings.IsEmpty() {
|
||||
jsonProf.ConfigurationWebURL = enrollURL
|
||||
} else {
|
||||
depProfile.ConfigurationWebURL = appConfig.ServerSettings.ServerURL + "/mdm/sso"
|
||||
jsonProf.ConfigurationWebURL = appCfg.ServerSettings.ServerURL + "/mdm/sso"
|
||||
}
|
||||
|
||||
depClient := NewDEPClient(d.depStorage, d.ds, d.logger)
|
||||
res, err := depClient.DefineProfile(ctx, DEPName, depProfile)
|
||||
res, err := depClient.DefineProfile(ctx, DEPName, jsonProf)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "apple POST /profile request failed")
|
||||
}
|
||||
|
||||
if err := d.depStorage.StoreAssignerProfile(ctx, DEPName, res.ProfileUUID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "set profile UUID")
|
||||
if setupAsst != nil {
|
||||
setupAsst.ProfileUUID = res.ProfileUUID
|
||||
if err := d.ds.SetMDMAppleSetupAssistantProfileUUID(ctx, setupAsst.TeamID, res.ProfileUUID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "save setup assistant profile UUID")
|
||||
}
|
||||
} else {
|
||||
// for backwards compatibility, we store the profile UUID of the default
|
||||
// profile in the nanomdm storage.
|
||||
if err := d.depStorage.StoreAssignerProfile(ctx, DEPName, res.ProfileUUID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "save default profile UUID")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnrollURL returns an URL that can be used to obtain an MDM enrollment
|
||||
// profile (xml) from Fleet.
|
||||
func (d *DEPService) EnrollURL(token string) (string, error) {
|
||||
appConfig, err := d.ds.AppConfig(context.Background())
|
||||
func (d *DEPService) ensureDefaultSetupAssistant(ctx context.Context) (string, time.Time, error) {
|
||||
profileUUID, profileModTime, err := d.depStorage.RetrieveAssignerProfile(ctx, DEPName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get app config: %w", err)
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
if profileUUID == "" {
|
||||
d.logger.Log("msg", "default DEP profile not set, creating")
|
||||
if err := d.CreateDefaultProfile(ctx); err != nil {
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
profileUUID, profileModTime, err = d.depStorage.RetrieveAssignerProfile(ctx, DEPName)
|
||||
if err != nil {
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
}
|
||||
return profileUUID, profileModTime, nil
|
||||
}
|
||||
|
||||
func (d *DEPService) ensureCustomSetupAssistantIfExists(ctx context.Context, tmID *uint) (string, time.Time, error) {
|
||||
asst, err := d.ds.GetMDMAppleSetupAssistant(ctx, tmID)
|
||||
if err != nil {
|
||||
if fleet.IsNotFound(err) {
|
||||
// no error, no custom setup assistant for that team
|
||||
return "", time.Time{}, nil
|
||||
}
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
|
||||
return EnrollURL(token, appConfig)
|
||||
if asst.ProfileUUID == "" {
|
||||
if err := d.RegisterProfileWithAppleDEPServer(ctx, asst); err != nil {
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
}
|
||||
return asst.ProfileUUID, asst.UploadedAt, nil
|
||||
}
|
||||
|
||||
func (d *DEPService) RunAssigner(ctx context.Context) error {
|
||||
profileUUID, profileModTime, err := d.depStorage.RetrieveAssignerProfile(ctx, DEPName)
|
||||
// ensure the default (fallback) setup assistant profile exists, registered
|
||||
// with Apple DEP.
|
||||
_, defModTime, err := d.ensureDefaultSetupAssistant(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if profileUUID == "" {
|
||||
d.logger.Log("msg", "DEP profile not set, creating one with default values")
|
||||
|
||||
// Note: This is likely to change once
|
||||
// https://github.com/fleetdm/fleet/issues/10518 is defined and
|
||||
// ready to develop. We'll have different DEP profiles per
|
||||
// team, and we'll have to grab the right DEP profile for whatever
|
||||
// AppConfig.MDM.AppleBMDefaultTeam is.
|
||||
//
|
||||
// I'm thinking that the default profile will be created along with
|
||||
// the team, but that still TBD based on the designs of the CLI
|
||||
// and the API.
|
||||
if err := d.CreateDefaultProfile(ctx); err != nil {
|
||||
// get the Apple BM default team and if it has a custom setup assistant,
|
||||
// ensure it is registered with Apple DEP.
|
||||
appCfg, err := d.ds.AppConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var customTeamID *uint
|
||||
if appCfg.MDM.AppleBMDefaultTeam != "" {
|
||||
tm, err := d.ds.TeamByName(ctx, appCfg.MDM.AppleBMDefaultTeam)
|
||||
// NOTE: TeamByName does NOT return a not found error if it does not exist
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return err
|
||||
}
|
||||
profileModTime = time.Now()
|
||||
if tm != nil {
|
||||
customTeamID = &tm.ID
|
||||
}
|
||||
}
|
||||
customUUID, customModTime, err := d.ensureCustomSetupAssistantIfExists(ctx, customTeamID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get the modification timestamp of the effective profile (custom or default)
|
||||
effectiveProfModTime := defModTime
|
||||
if customUUID != "" {
|
||||
effectiveProfModTime = customModTime
|
||||
}
|
||||
|
||||
cursor, cursorModTime, err := d.depStorage.RetrieveCursor(ctx, DEPName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// If the DEP Profile was changed since last sync then we clear
|
||||
|
||||
// If the effective profile was changed since last sync then we clear
|
||||
// the cursor and perform a full sync of all devices and profile assigning.
|
||||
if cursor != "" && profileModTime.After(cursorModTime) {
|
||||
if cursor != "" && effectiveProfModTime.After(cursorModTime) {
|
||||
d.logger.Log("msg", "clearing device syncer cursor")
|
||||
if err := d.depStorage.StoreCursor(ctx, DEPName, ""); err != nil {
|
||||
return err
|
||||
|
|
@ -258,26 +329,19 @@ func NewDEPService(
|
|||
loggingDebug bool,
|
||||
) *DEPService {
|
||||
depClient := NewDEPClient(depStorage, ds, logger)
|
||||
assignerOpts := []depsync.AssignerOption{
|
||||
depsync.WithAssignerLogger(logging.NewNanoDEPLogger(kitlog.With(logger, "component", "nanodep-assigner"))),
|
||||
depSvc := &DEPService{
|
||||
depStorage: depStorage,
|
||||
logger: logger,
|
||||
ds: ds,
|
||||
}
|
||||
if loggingDebug {
|
||||
assignerOpts = append(assignerOpts, depsync.WithDebug())
|
||||
}
|
||||
assigner := depsync.NewAssigner(
|
||||
depClient,
|
||||
DEPName,
|
||||
depStorage,
|
||||
assignerOpts...,
|
||||
)
|
||||
|
||||
syncer := depsync.NewSyncer(
|
||||
depSvc.syncer = depsync.NewSyncer(
|
||||
depClient,
|
||||
DEPName,
|
||||
depStorage,
|
||||
depsync.WithLogger(logging.NewNanoDEPLogger(kitlog.With(logger, "component", "nanodep-syncer"))),
|
||||
depsync.WithCallback(func(ctx context.Context, isFetch bool, resp *godep.DeviceResponse) error {
|
||||
n, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, resp.Devices)
|
||||
n, teamID, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, resp.Devices)
|
||||
switch {
|
||||
case err != nil:
|
||||
level.Error(kitlog.With(logger)).Log("err", err)
|
||||
|
|
@ -288,18 +352,112 @@ func NewDEPService(
|
|||
level.Info(kitlog.With(logger)).Log("msg", "no DEP hosts to add")
|
||||
}
|
||||
|
||||
// TODO(mna): at this point, the hosts rows are created for the devices, with the
|
||||
// at this point, the hosts rows are created for the devices, with the
|
||||
// correct team_id, so we know what team-specific profile needs to be applied.
|
||||
return assigner.ProcessDeviceResponse(ctx, resp)
|
||||
return depSvc.processDeviceResponse(ctx, depClient, resp, teamID)
|
||||
}),
|
||||
)
|
||||
|
||||
return &DEPService{
|
||||
syncer: syncer,
|
||||
depStorage: depStorage,
|
||||
logger: logger,
|
||||
ds: ds,
|
||||
return depSvc
|
||||
}
|
||||
|
||||
// processDeviceResponse processes the device response from the device sync
|
||||
// DEP API endpoints and assigns the profile UUID associated with the DEP
|
||||
// client DEP name.
|
||||
func (d *DEPService) processDeviceResponse(ctx context.Context, depClient *godep.Client, resp *godep.DeviceResponse, tmID *uint) error {
|
||||
if len(resp.Devices) < 1 {
|
||||
// no devices means we can't assign anything
|
||||
return nil
|
||||
}
|
||||
|
||||
// get profile uuid of tmID or default
|
||||
profUUID, _, err := d.ensureCustomSetupAssistantIfExists(ctx, tmID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ensure setup assistant for team %v: %w", tmID, err)
|
||||
}
|
||||
if profUUID == "" {
|
||||
profUUID, _, err = d.ensureDefaultSetupAssistant(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ensure default setup assistant: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if profUUID == "" {
|
||||
level.Debug(d.logger).Log("msg", "empty assigner profile UUID")
|
||||
return nil
|
||||
}
|
||||
|
||||
var serials []string
|
||||
for _, device := range resp.Devices {
|
||||
level.Debug(d.logger).Log(
|
||||
"msg", "device",
|
||||
"serial_number", device.SerialNumber,
|
||||
"device_assigned_by", device.DeviceAssignedBy,
|
||||
"device_assigned_date", device.DeviceAssignedDate,
|
||||
"op_date", device.OpDate,
|
||||
"op_type", device.OpType,
|
||||
"profile_assign_time", device.ProfileAssignTime,
|
||||
"push_push_time", device.ProfilePushTime,
|
||||
"profile_uuid", device.ProfileUUID,
|
||||
)
|
||||
// We currently only listen for an op_type of "added", the other
|
||||
// op_types are ambiguous and it would be needless to assign the
|
||||
// profile UUID every single time we get an update.
|
||||
if strings.ToLower(device.OpType) == "added" ||
|
||||
// The op_type field is only applicable with the SyncDevices API call,
|
||||
// Empty op_type come from the first call to FetchDevices without a cursor,
|
||||
// and we do want to assign profiles to them.
|
||||
strings.ToLower(device.OpType) == "" {
|
||||
serials = append(serials, device.SerialNumber)
|
||||
}
|
||||
}
|
||||
|
||||
logger := kitlog.With(d.logger, "profile_uuid", profUUID)
|
||||
|
||||
if len(serials) < 1 {
|
||||
level.Debug(logger).Log(
|
||||
"msg", "no serials to assign",
|
||||
"devices", len(resp.Devices),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
apiResp, err := depClient.AssignProfile(ctx, DEPName, profUUID, serials...)
|
||||
if err != nil {
|
||||
level.Info(logger).Log(
|
||||
"msg", "assign profile",
|
||||
"devices", len(serials),
|
||||
"err", err,
|
||||
)
|
||||
return fmt.Errorf("assign profile: %w", err)
|
||||
}
|
||||
|
||||
logs := []interface{}{
|
||||
"msg", "profile assigned",
|
||||
"devices", len(serials),
|
||||
}
|
||||
logs = append(logs, logCountsForResults(apiResp.Devices)...)
|
||||
level.Info(logger).Log(logs...)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// logCountsForResults tries to aggregate the result types and log the counts.
|
||||
func logCountsForResults(deviceResults map[string]string) (out []interface{}) {
|
||||
results := map[string]int{"success": 0, "not_accessible": 0, "failed": 0, "other": 0}
|
||||
for _, result := range deviceResults {
|
||||
l := strings.ToLower(result)
|
||||
if _, ok := results[l]; !ok {
|
||||
l = "other"
|
||||
}
|
||||
results[l] += 1
|
||||
}
|
||||
for k, v := range results {
|
||||
if v > 0 {
|
||||
out = append(out, k, v)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// NewDEPClient creates an Apple DEP API HTTP client based on the provided
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package apple_mdm
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
|
@ -25,7 +24,7 @@ func TestDEPService(t *testing.T) {
|
|||
logger := log.NewNopLogger()
|
||||
depStorage := new(nanodep_mock.Storage)
|
||||
depSvc := NewDEPService(ds, depStorage, logger, true)
|
||||
defaultProfile := depSvc.GetDefaultProfile()
|
||||
defaultProfile := depSvc.getDefaultProfile()
|
||||
serverURL := "https://example.com/"
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -45,6 +44,8 @@ func TestDEPService(t *testing.T) {
|
|||
got.URL = ""
|
||||
got.ConfigurationWebURL = ""
|
||||
require.Equal(t, defaultProfile, &got)
|
||||
default:
|
||||
require.Fail(t, "unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
|
@ -55,12 +56,23 @@ func TestDEPService(t *testing.T) {
|
|||
return appCfg, nil
|
||||
}
|
||||
|
||||
var savedProfile fleet.MDMAppleEnrollmentProfile
|
||||
ds.NewMDMAppleEnrollmentProfileFunc = func(ctx context.Context, p fleet.MDMAppleEnrollmentProfilePayload) (*fleet.MDMAppleEnrollmentProfile, error) {
|
||||
require.Equal(t, fleet.MDMAppleEnrollmentType("automatic"), p.Type)
|
||||
require.Equal(t, fleet.MDMAppleEnrollmentTypeAutomatic, p.Type)
|
||||
require.NotEmpty(t, p.Token)
|
||||
//require.JSONEq
|
||||
return &fleet.MDMAppleEnrollmentProfile{}, nil
|
||||
res := &fleet.MDMAppleEnrollmentProfile{
|
||||
Token: p.Token,
|
||||
Type: p.Type,
|
||||
DEPProfile: p.DEPProfile,
|
||||
}
|
||||
savedProfile = *res
|
||||
return res, nil
|
||||
}
|
||||
|
||||
ds.GetMDMAppleEnrollmentProfileByTypeFunc = func(ctx context.Context, typ fleet.MDMAppleEnrollmentType) (*fleet.MDMAppleEnrollmentProfile, error) {
|
||||
require.Equal(t, fleet.MDMAppleEnrollmentTypeAutomatic, typ)
|
||||
res := savedProfile
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
ds.SaveAppConfigFunc = func(ctx context.Context, info *fleet.AppConfig) error {
|
||||
|
|
@ -84,50 +96,18 @@ func TestDEPService(t *testing.T) {
|
|||
err := depSvc.CreateDefaultProfile(ctx)
|
||||
require.NoError(t, err)
|
||||
require.True(t, ds.NewMDMAppleEnrollmentProfileFuncInvoked)
|
||||
require.True(t, ds.GetMDMAppleEnrollmentProfileByTypeFuncInvoked)
|
||||
require.True(t, depStorage.RetrieveConfigFuncInvoked)
|
||||
require.True(t, depStorage.StoreAssignerProfileFuncInvoked)
|
||||
})
|
||||
|
||||
t.Run("EnrollURL", func(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
logger := log.NewNopLogger()
|
||||
depStorage := new(nanodep_mock.Storage)
|
||||
depSvc := NewDEPService(ds, depStorage, logger, true)
|
||||
testErr := errors.New("test")
|
||||
serverURL := "https://example.com/"
|
||||
const serverURL = "https://example.com/"
|
||||
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return nil, testErr
|
||||
}
|
||||
|
||||
url, err := depSvc.EnrollURL("token")
|
||||
require.ErrorIs(t, err, testErr)
|
||||
require.Empty(t, url)
|
||||
require.True(t, ds.AppConfigFuncInvoked)
|
||||
ds.AppConfigFuncInvoked = false
|
||||
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
appCfg := &fleet.AppConfig{}
|
||||
appCfg.ServerSettings.ServerURL = " http://foo.com"
|
||||
return appCfg, nil
|
||||
}
|
||||
|
||||
url, err = depSvc.EnrollURL("token")
|
||||
require.Error(t, err)
|
||||
require.Empty(t, url)
|
||||
require.True(t, ds.AppConfigFuncInvoked)
|
||||
ds.AppConfigFuncInvoked = false
|
||||
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
appCfg := &fleet.AppConfig{}
|
||||
appCfg.ServerSettings.ServerURL = serverURL
|
||||
return appCfg, nil
|
||||
}
|
||||
|
||||
url, err = depSvc.EnrollURL("token")
|
||||
appCfg := &fleet.AppConfig{}
|
||||
appCfg.ServerSettings.ServerURL = serverURL
|
||||
url, err := EnrollURL("token", appCfg)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, url, serverURL+"api/mdm/apple/enroll?token=token")
|
||||
require.True(t, ds.AppConfigFuncInvoked)
|
||||
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -536,6 +536,8 @@ type NewMDMAppleEnrollmentProfileFunc func(ctx context.Context, enrollmentPayloa
|
|||
|
||||
type GetMDMAppleEnrollmentProfileByTokenFunc func(ctx context.Context, token string) (*fleet.MDMAppleEnrollmentProfile, error)
|
||||
|
||||
type GetMDMAppleEnrollmentProfileByTypeFunc func(ctx context.Context, typ fleet.MDMAppleEnrollmentType) (*fleet.MDMAppleEnrollmentProfile, error)
|
||||
|
||||
type ListMDMAppleEnrollmentProfilesFunc func(ctx context.Context) ([]*fleet.MDMAppleEnrollmentProfile, error)
|
||||
|
||||
type GetMDMAppleCommandResultsFunc func(ctx context.Context, commandUUID string) ([]*fleet.MDMAppleCommandResult, error)
|
||||
|
|
@ -558,7 +560,7 @@ type BatchSetMDMAppleProfilesFunc func(ctx context.Context, tmID *uint, profiles
|
|||
|
||||
type MDMAppleListDevicesFunc func(ctx context.Context) ([]fleet.MDMAppleDevice, error)
|
||||
|
||||
type IngestMDMAppleDevicesFromDEPSyncFunc func(ctx context.Context, devices []godep.Device) (int64, error)
|
||||
type IngestMDMAppleDevicesFromDEPSyncFunc func(ctx context.Context, devices []godep.Device) (int64, *uint, error)
|
||||
|
||||
type IngestMDMAppleDeviceFromCheckinFunc func(ctx context.Context, mdmHost fleet.MDMAppleHostDetails) error
|
||||
|
||||
|
|
@ -1399,6 +1401,9 @@ type DataStore struct {
|
|||
GetMDMAppleEnrollmentProfileByTokenFunc GetMDMAppleEnrollmentProfileByTokenFunc
|
||||
GetMDMAppleEnrollmentProfileByTokenFuncInvoked bool
|
||||
|
||||
GetMDMAppleEnrollmentProfileByTypeFunc GetMDMAppleEnrollmentProfileByTypeFunc
|
||||
GetMDMAppleEnrollmentProfileByTypeFuncInvoked bool
|
||||
|
||||
ListMDMAppleEnrollmentProfilesFunc ListMDMAppleEnrollmentProfilesFunc
|
||||
ListMDMAppleEnrollmentProfilesFuncInvoked bool
|
||||
|
||||
|
|
@ -3345,6 +3350,13 @@ func (s *DataStore) GetMDMAppleEnrollmentProfileByToken(ctx context.Context, tok
|
|||
return s.GetMDMAppleEnrollmentProfileByTokenFunc(ctx, token)
|
||||
}
|
||||
|
||||
func (s *DataStore) GetMDMAppleEnrollmentProfileByType(ctx context.Context, typ fleet.MDMAppleEnrollmentType) (*fleet.MDMAppleEnrollmentProfile, error) {
|
||||
s.mu.Lock()
|
||||
s.GetMDMAppleEnrollmentProfileByTypeFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.GetMDMAppleEnrollmentProfileByTypeFunc(ctx, typ)
|
||||
}
|
||||
|
||||
func (s *DataStore) ListMDMAppleEnrollmentProfiles(ctx context.Context) ([]*fleet.MDMAppleEnrollmentProfile, error) {
|
||||
s.mu.Lock()
|
||||
s.ListMDMAppleEnrollmentProfilesFuncInvoked = true
|
||||
|
|
@ -3422,7 +3434,7 @@ func (s *DataStore) MDMAppleListDevices(ctx context.Context) ([]fleet.MDMAppleDe
|
|||
return s.MDMAppleListDevicesFunc(ctx)
|
||||
}
|
||||
|
||||
func (s *DataStore) IngestMDMAppleDevicesFromDEPSync(ctx context.Context, devices []godep.Device) (int64, error) {
|
||||
func (s *DataStore) IngestMDMAppleDevicesFromDEPSync(ctx context.Context, devices []godep.Device) (int64, *uint, error) {
|
||||
s.mu.Lock()
|
||||
s.IngestMDMAppleDevicesFromDEPSyncFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
|
|
|
|||
|
|
@ -407,7 +407,7 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle
|
|||
appConfig.MDM.EndUserAuthentication.SSOProviderSettings
|
||||
serverURLChanged := oldAppConfig.ServerSettings.ServerURL != appConfig.ServerSettings.ServerURL
|
||||
if (mdmSSOSettingsChanged || serverURLChanged) && license.Tier == "premium" {
|
||||
if err := svc.EnterpriseOverrides.MDMAppleSyncDEPProfile(ctx); err != nil {
|
||||
if err := svc.EnterpriseOverrides.MDMAppleSyncDEPProfiles(ctx); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "sync DEP profile")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ package service
|
|||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
|
@ -941,14 +941,15 @@ func TestMDMAppleConfig(t *testing.T) {
|
|||
if tt.findTeam {
|
||||
return &fleet.Team{}, nil
|
||||
}
|
||||
return nil, errors.New(notFoundErr)
|
||||
}
|
||||
ds.ListMDMAppleEnrollmentProfilesFunc = func(ctx context.Context) ([]*fleet.MDMAppleEnrollmentProfile, error) {
|
||||
return []*fleet.MDMAppleEnrollmentProfile{}, nil
|
||||
return nil, sql.ErrNoRows
|
||||
}
|
||||
ds.NewMDMAppleEnrollmentProfileFunc = func(ctx context.Context, enrollmentPayload fleet.MDMAppleEnrollmentProfilePayload) (*fleet.MDMAppleEnrollmentProfile, error) {
|
||||
return &fleet.MDMAppleEnrollmentProfile{}, nil
|
||||
}
|
||||
ds.GetMDMAppleEnrollmentProfileByTypeFunc = func(ctx context.Context, typ fleet.MDMAppleEnrollmentType) (*fleet.MDMAppleEnrollmentProfile, error) {
|
||||
raw := json.RawMessage("{}")
|
||||
return &fleet.MDMAppleEnrollmentProfile{DEPProfile: &raw}, nil
|
||||
}
|
||||
|
||||
depStorage.RetrieveConfigFunc = func(p0 context.Context, p1 string) (*nanodep_client.Config, error) {
|
||||
return &nanodep_client.Config{BaseURL: depSrv.URL}, nil
|
||||
|
|
|
|||
|
|
@ -36,119 +36,6 @@ import (
|
|||
"github.com/micromdm/nanomdm/mdm"
|
||||
)
|
||||
|
||||
type createMDMAppleEnrollmentProfileRequest struct {
|
||||
Type fleet.MDMAppleEnrollmentType `json:"type"`
|
||||
DEPProfile *json.RawMessage `json:"dep_profile"`
|
||||
}
|
||||
|
||||
type createMDMAppleEnrollmentProfileResponse struct {
|
||||
EnrollmentProfile *fleet.MDMAppleEnrollmentProfile `json:"enrollment_profile"`
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r createMDMAppleEnrollmentProfileResponse) error() error { return r.Err }
|
||||
|
||||
func createMDMAppleEnrollmentProfilesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
||||
req := request.(*createMDMAppleEnrollmentProfileRequest)
|
||||
|
||||
enrollmentProfile, err := svc.NewMDMAppleEnrollmentProfile(ctx, fleet.MDMAppleEnrollmentProfilePayload{
|
||||
Type: req.Type,
|
||||
DEPProfile: req.DEPProfile,
|
||||
})
|
||||
if err != nil {
|
||||
return createMDMAppleEnrollmentProfileResponse{
|
||||
Err: err,
|
||||
}, nil
|
||||
}
|
||||
return createMDMAppleEnrollmentProfileResponse{
|
||||
EnrollmentProfile: enrollmentProfile,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) NewMDMAppleEnrollmentProfile(ctx context.Context, enrollmentPayload fleet.MDMAppleEnrollmentProfilePayload) (*fleet.MDMAppleEnrollmentProfile, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.MDMAppleEnrollmentProfile{}, fleet.ActionWrite); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
|
||||
appConfig, err := svc.ds.AppConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
|
||||
// generate a token for the profile
|
||||
enrollmentPayload.Token = uuid.New().String()
|
||||
|
||||
profile, err := svc.ds.NewMDMAppleEnrollmentProfile(ctx, enrollmentPayload)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
if profile.DEPProfile != nil {
|
||||
lic, err := svc.License(ctx)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "get license")
|
||||
}
|
||||
if !lic.IsPremium() {
|
||||
return nil, fleet.ErrMissingLicense
|
||||
}
|
||||
if err := svc.EnterpriseOverrides.MDMAppleSyncDEPProfile(ctx); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
enrollmentURL, err := apple_mdm.EnrollURL(profile.Token, appConfig)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
profile.EnrollmentURL = enrollmentURL
|
||||
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
type listMDMAppleEnrollmentProfilesRequest struct{}
|
||||
|
||||
type listMDMAppleEnrollmentProfilesResponse struct {
|
||||
EnrollmentProfiles []*fleet.MDMAppleEnrollmentProfile `json:"enrollment_profiles"`
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r listMDMAppleEnrollmentProfilesResponse) error() error { return r.Err }
|
||||
|
||||
func listMDMAppleEnrollmentsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
||||
enrollmentProfiles, err := svc.ListMDMAppleEnrollmentProfiles(ctx)
|
||||
if err != nil {
|
||||
return listMDMAppleEnrollmentProfilesResponse{
|
||||
Err: err,
|
||||
}, nil
|
||||
}
|
||||
return listMDMAppleEnrollmentProfilesResponse{
|
||||
EnrollmentProfiles: enrollmentProfiles,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) ListMDMAppleEnrollmentProfiles(ctx context.Context) ([]*fleet.MDMAppleEnrollmentProfile, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.MDMAppleEnrollmentProfile{}, fleet.ActionWrite); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
|
||||
appConfig, err := svc.ds.AppConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
|
||||
enrollments, err := svc.ds.ListMDMAppleEnrollmentProfiles(ctx)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
for i := range enrollments {
|
||||
enrollURL, err := apple_mdm.EnrollURL(enrollments[i].Token, appConfig)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
enrollments[i].EnrollmentURL = enrollURL
|
||||
}
|
||||
return enrollments, nil
|
||||
}
|
||||
|
||||
type getMDMAppleCommandResultsRequest struct {
|
||||
CommandUUID string `query:"command_uuid,optional"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -190,11 +190,7 @@ func TestAppleMDMAuthorization(t *testing.T) {
|
|||
|
||||
testAuthdMethods := func(t *testing.T, user *fleet.User, shouldFailWithAuth bool) {
|
||||
ctx := test.UserContext(ctx, user)
|
||||
_, err := svc.NewMDMAppleEnrollmentProfile(ctx, fleet.MDMAppleEnrollmentProfilePayload{})
|
||||
checkAuthErr(t, err, shouldFailWithAuth)
|
||||
_, err = svc.ListMDMAppleEnrollmentProfiles(ctx)
|
||||
checkAuthErr(t, err, shouldFailWithAuth)
|
||||
_, err = svc.UploadMDMAppleInstaller(ctx, "foo", 3, bytes.NewReader([]byte("foo")))
|
||||
_, err := svc.UploadMDMAppleInstaller(ctx, "foo", 3, bytes.NewReader([]byte("foo")))
|
||||
checkAuthErr(t, err, shouldFailWithAuth)
|
||||
_, err = svc.GetMDMAppleInstallerByID(ctx, 42)
|
||||
checkAuthErr(t, err, shouldFailWithAuth)
|
||||
|
|
@ -811,29 +807,6 @@ func TestHostDetailsMDMProfiles(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAppleMDMEnrollmentProfile(t *testing.T) {
|
||||
svc, ctx, _ := setupAppleMDMService(t)
|
||||
|
||||
// Only global admins can create enrollment profiles.
|
||||
ctx = test.UserContext(ctx, test.UserAdmin)
|
||||
_, err := svc.NewMDMAppleEnrollmentProfile(ctx, fleet.MDMAppleEnrollmentProfilePayload{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// All other users should not have access to the endpoints.
|
||||
for _, user := range []*fleet.User{
|
||||
test.UserNoRoles,
|
||||
test.UserMaintainer,
|
||||
test.UserObserver,
|
||||
test.UserObserverPlus,
|
||||
test.UserTeamAdminTeam1,
|
||||
} {
|
||||
ctx := test.UserContext(ctx, user)
|
||||
_, err := svc.NewMDMAppleEnrollmentProfile(ctx, fleet.MDMAppleEnrollmentProfilePayload{})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), authz.ForbiddenErrorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMDMCommandAuthz(t *testing.T) {
|
||||
svc, ctx, ds := setupAppleMDMService(t)
|
||||
|
||||
|
|
|
|||
|
|
@ -459,8 +459,6 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
|
|||
|
||||
// TODO: are those undocumented endpoints still needed? I think they were only used
|
||||
// by 'fleetctl apple-mdm' sub-commands.
|
||||
mdm.POST("/api/_version_/fleet/mdm/apple/enrollmentprofiles", createMDMAppleEnrollmentProfilesEndpoint, createMDMAppleEnrollmentProfileRequest{})
|
||||
mdm.GET("/api/_version_/fleet/mdm/apple/enrollmentprofiles", listMDMAppleEnrollmentsEndpoint, listMDMAppleEnrollmentProfilesRequest{})
|
||||
mdm.POST("/api/_version_/fleet/mdm/apple/installers", uploadAppleInstallerEndpoint, uploadAppleInstallerRequest{})
|
||||
mdm.GET("/api/_version_/fleet/mdm/apple/installers/{installer_id:[0-9]+}", getAppleInstallerEndpoint, getAppleInstallerDetailsRequest{})
|
||||
mdm.DELETE("/api/_version_/fleet/mdm/apple/installers/{installer_id:[0-9]+}", deleteAppleInstallerEndpoint, deleteAppleInstallerDetailsRequest{})
|
||||
|
|
|
|||
|
|
@ -557,8 +557,6 @@ func mockSuccessfulPush(pushes []*mdm.Push) (map[string]*push.Response, error) {
|
|||
|
||||
func mdmAppleConfigurationRequiredEndpoints() [][2]string {
|
||||
return [][2]string{
|
||||
{"POST", "/api/latest/fleet/mdm/apple/enrollmentprofiles"},
|
||||
{"GET", "/api/latest/fleet/mdm/apple/enrollmentprofiles"},
|
||||
{"POST", "/api/latest/fleet/mdm/apple/enqueue"},
|
||||
{"GET", "/api/latest/fleet/mdm/apple/commandresults"},
|
||||
{"GET", "/api/latest/fleet/mdm/apple/installers/1"},
|
||||
|
|
|
|||
Loading…
Reference in a new issue