From 70f18dda4a8e1f3eec3e43dbe4ba99bf855ad283 Mon Sep 17 00:00:00 2001 From: Martin Angers Date: Tue, 9 May 2023 13:00:18 -0400 Subject: [PATCH] Apply custom setup assistants (if present) when ingesting new devices (#11563) --- ...5-integrate-setup-assistant-with-apple-dep | 1 + ee/server/service/mdm.go | 53 +--- ee/server/service/service.go | 2 +- mdm_profiles/setup_assistant.json | 28 ++ server/datastore/mysql/apple_mdm.go | 45 ++- server/datastore/mysql/apple_mdm_test.go | 73 ++++- server/datastore/mysql/hosts_test.go | 3 +- server/fleet/datastore.go | 11 +- server/fleet/service.go | 9 +- server/mdm/apple/apple_mdm.go | 286 ++++++++++++++---- server/mdm/apple/apple_mdm_test.go | 64 ++-- server/mock/datastore_mock.go | 16 +- server/service/appconfig.go | 2 +- server/service/appconfig_test.go | 11 +- server/service/apple_mdm.go | 113 ------- server/service/apple_mdm_test.go | 29 +- server/service/handler.go | 2 - server/service/testing_utils.go | 2 - 18 files changed, 423 insertions(+), 327 deletions(-) create mode 100644 changes/issue-10995-integrate-setup-assistant-with-apple-dep create mode 100644 mdm_profiles/setup_assistant.json diff --git a/changes/issue-10995-integrate-setup-assistant-with-apple-dep b/changes/issue-10995-integrate-setup-assistant-with-apple-dep new file mode 100644 index 0000000000..2cdf6e906b --- /dev/null +++ b/changes/issue-10995-integrate-setup-assistant-with-apple-dep @@ -0,0 +1 @@ +* Integrated the macOS setup assistant feature with Apple DEP so that the setup assistants are assigned to the enrolled devices. diff --git a/ee/server/service/mdm.go b/ee/server/service/mdm.go index 65ebb2c27f..1ee9599e49 100644 --- a/ee/server/service/mdm.go +++ b/ee/server/service/mdm.go @@ -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 } diff --git a/ee/server/service/service.go b/ee/server/service/service.go index 63112ed48f..4c92f91282 100644 --- a/ee/server/service/service.go +++ b/ee/server/service/service.go @@ -69,7 +69,7 @@ func NewService( MDMAppleEnableFileVaultAndEscrow: eeservice.MDMAppleEnableFileVaultAndEscrow, MDMAppleDisableFileVaultAndEscrow: eeservice.MDMAppleDisableFileVaultAndEscrow, DeleteMDMAppleSetupAssistant: eeservice.DeleteMDMAppleSetupAssistant, - MDMAppleSyncDEPProfile: eeservice.mdmAppleSyncDEPProfile, + MDMAppleSyncDEPProfiles: eeservice.mdmAppleSyncDEPProfiles, DeleteMDMAppleBootstrapPackage: eeservice.DeleteMDMAppleBootstrapPackage, }) diff --git a/mdm_profiles/setup_assistant.json b/mdm_profiles/setup_assistant.json new file mode 100644 index 0000000000..4f96ca2652 --- /dev/null +++ b/mdm_profiles/setup_assistant.json @@ -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" + ] +} diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go index 76c978916b..248eb3e0db 100644 --- a/server/datastore/mysql/apple_mdm.go +++ b/server/datastore/mysql/apple_mdm.go @@ -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 { diff --git a/server/datastore/mysql/apple_mdm_test.go b/server/datastore/mysql/apple_mdm_test.go index f9fc872fd0..128f8d5e46 100644 --- a/server/datastore/mysql/apple_mdm_test.go +++ b/server/datastore/mysql/apple_mdm_test.go @@ -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) +} diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index 16facdc030..ca07e58797 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -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 diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 9b4152a540..94cd6b29ce 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -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. diff --git a/server/fleet/service.go b/server/fleet/service.go index c81f9612ca..d19cac0f6e 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -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 diff --git a/server/mdm/apple/apple_mdm.go b/server/mdm/apple/apple_mdm.go index 3724479bac..8800002e9a 100644 --- a/server/mdm/apple/apple_mdm.go +++ b/server/mdm/apple/apple_mdm.go @@ -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 diff --git a/server/mdm/apple/apple_mdm_test.go b/server/mdm/apple/apple_mdm_test.go index ee143aa0da..692da4e7a1 100644 --- a/server/mdm/apple/apple_mdm_test.go +++ b/server/mdm/apple/apple_mdm_test.go @@ -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) - }) } diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 78e6c4d2b3..f6a8214c99 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -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() diff --git a/server/service/appconfig.go b/server/service/appconfig.go index 7699b5ffdb..0689a4709c 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -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") } } diff --git a/server/service/appconfig_test.go b/server/service/appconfig_test.go index ad28940265..1db43e4dc7 100644 --- a/server/service/appconfig_test.go +++ b/server/service/appconfig_test.go @@ -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 diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 2da6cf4688..355931f416 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -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"` } diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index c534b64b23..b3bcbd3924 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -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) diff --git a/server/service/handler.go b/server/service/handler.go index 32aee8e7ea..722df1f7f3 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -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{}) diff --git a/server/service/testing_utils.go b/server/service/testing_utils.go index 0d1cfea04e..0646cd35d4 100644 --- a/server/service/testing_utils.go +++ b/server/service/testing_utils.go @@ -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"},