Apply custom setup assistants (if present) when ingesting new devices (#11563)

This commit is contained in:
Martin Angers 2023-05-09 13:00:18 -04:00 committed by GitHub
parent 487f8b6e1f
commit 70f18dda4a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 423 additions and 327 deletions

View file

@ -0,0 +1 @@
* Integrated the macOS setup assistant feature with Apple DEP so that the setup assistants are assigned to the enrolled devices.

View file

@ -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
}

View file

@ -69,7 +69,7 @@ func NewService(
MDMAppleEnableFileVaultAndEscrow: eeservice.MDMAppleEnableFileVaultAndEscrow,
MDMAppleDisableFileVaultAndEscrow: eeservice.MDMAppleDisableFileVaultAndEscrow,
DeleteMDMAppleSetupAssistant: eeservice.DeleteMDMAppleSetupAssistant,
MDMAppleSyncDEPProfile: eeservice.mdmAppleSyncDEPProfile,
MDMAppleSyncDEPProfiles: eeservice.mdmAppleSyncDEPProfiles,
DeleteMDMAppleBootstrapPackage: eeservice.DeleteMDMAppleBootstrapPackage,
})

View 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"
]
}

View file

@ -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 {

View file

@ -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)
}

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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)
})
}

View file

@ -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()

View file

@ -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")
}
}

View file

@ -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

View file

@ -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"`
}

View file

@ -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)

View file

@ -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{})

View file

@ -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"},