fleet/server/mdm/apple/apple_mdm.go

776 lines
26 KiB
Go

package apple_mdm
import (
"bytes"
"context"
"encoding/json"
"encoding/xml"
"fmt"
"strings"
"text/template"
"time"
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/logging"
"github.com/fleetdm/fleet/v4/server/mdm/internal/commonmdm"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/getsentry/sentry-go"
"github.com/go-kit/log/level"
"github.com/google/uuid"
"github.com/micromdm/nanodep/godep"
kitlog "github.com/go-kit/kit/log"
nanodep_storage "github.com/micromdm/nanodep/storage"
depsync "github.com/micromdm/nanodep/sync"
)
// DEPName is the identifier/name used in nanodep MySQL storage which
// holds the DEP configuration.
//
// Fleet uses only one DEP configuration set for the whole deployment.
const DEPName = "fleet"
const (
// SCEPPath is Fleet's HTTP path for the SCEP service.
SCEPPath = "/mdm/apple/scep"
// MDMPath is Fleet's HTTP path for the core MDM service.
MDMPath = "/mdm/apple/mdm"
// EnrollPath is the HTTP path that serves the mobile profile to devices when enrolling.
EnrollPath = "/api/mdm/apple/enroll"
// InstallerPath is the HTTP path that serves installers to Apple devices.
InstallerPath = "/api/mdm/apple/installer"
// FleetUISSOCallbackPath is the front-end route used to
// redirect after the SSO flow is completed.
FleetUISSOCallbackPath = "/mdm/sso/callback"
// FleetPayloadIdentifier is the value for the "<key>PayloadIdentifier</key>"
// used by Fleet MDM on the enrollment profile.
FleetPayloadIdentifier = "com.fleetdm.fleet.mdm.apple"
// FleetdPublicManifestURL contains a valid manifest that can be used
// by InstallEnterpriseApplication to install `fleetd` in a host.
FleetdPublicManifestURL = "https://download.fleetdm.com/fleetd-base-manifest.plist"
)
func ResolveAppleMDMURL(serverURL string) (string, error) {
return commonmdm.ResolveURL(serverURL, MDMPath, false)
}
func ResolveAppleEnrollMDMURL(serverURL string) (string, error) {
return commonmdm.ResolveURL(serverURL, EnrollPath, false)
}
func ResolveAppleSCEPURL(serverURL string) (string, error) {
// Apple's SCEP client appends a query string to the SCEP URL in the
// enrollment profile, without checking if the URL already has a query
// string. Eg: if the URL is `/test/example?foo=bar` it'll make a
// request to `/test/example?foo=bar?SCEPOperation=..`
//
// As a consequence we ensure that the query is always clean for the SCEP URL.
return commonmdm.ResolveURL(serverURL, SCEPPath, true)
}
// DEPService is used to encapsulate tasks related to DEP enrollment.
//
// This service doesn't perform any authentication checks, so its suitable for
// internal usage within Fleet. If you need to expose any of the functionality
// to users, please make sure the caller is enforcing the right authorization
// checks.
type DEPService struct {
ds fleet.Datastore
depStorage nanodep_storage.AllStorage
syncer *depsync.Syncer
logger kitlog.Logger
}
// 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,
AutoAdvanceSetup: false,
AwaitDeviceConfigured: false,
IsSupervised: false,
IsMultiUser: false,
IsMandatory: false,
IsMDMRemovable: true,
Language: "en",
OrgMagic: "1",
Region: "US",
SkipSetupItems: []string{
"Accessibility",
"Appearance",
"AppleID",
"AppStore",
"Biometric",
"Diagnostics",
"FileVault",
"iCloudDiagnostics",
"iCloudStorage",
"Location",
"Payment",
"Privacy",
"Restore",
"ScreenTime",
"Siri",
"TermsOfAddress",
"TOS",
"UnlockWithWatch",
},
}
}
// createDefaultAutomaticProfile creates the default automatic (DEP) enrollment
// profile in mdm_apple_enrollment_profiles but does not register it with
// Apple. It also creates the authentication token to get enrollment profiles.
func (d *DEPService) createDefaultAutomaticProfile(ctx context.Context) error {
depProfile := d.getDefaultProfile()
token := uuid.New().String()
rawDEPProfile, err := json.Marshal(depProfile)
if err != nil {
return ctxerr.Wrap(ctx, err, "marshaling default profile")
}
payload := fleet.MDMAppleEnrollmentProfilePayload{
Token: token,
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")
}
return nil
}
// RegisterProfileWithAppleDEPServer registers the enrollment profile in
// Apple's servers via the DEP API, so it can be used for assignment. If
// setupAsst is nil, the default profile is registered. It assigns the
// up-to-date dynamic settings such as the server URL and MDM SSO URL if
// end-user authentication is enabled for that team/no-team.
func (d *DEPService) RegisterProfileWithAppleDEPServer(ctx context.Context, team *fleet.Team, setupAsst *fleet.MDMAppleSetupAssistant) error {
appCfg, err := d.ds.AppConfig(ctx)
if err != nil {
return ctxerr.Wrap(ctx, err, "fetching app config")
}
// 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
jsonProf.IsMDMRemovable = true // the default value defined by Apple is true
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.
//
// Even though the DEP profile supports an `url` attribute, we should
// 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.
jsonProf.ConfigurationWebURL = enrollURL
endUserAuthEnabled := appCfg.MDM.MacOSSetup.EnableEndUserAuthentication
if team != nil {
endUserAuthEnabled = team.Config.MDM.MacOSSetup.EnableEndUserAuthentication
}
if endUserAuthEnabled {
jsonProf.ConfigurationWebURL = appCfg.ServerSettings.ServerURL + "/mdm/sso"
}
depClient := NewDEPClient(d.depStorage, d.ds, d.logger)
res, err := depClient.DefineProfile(ctx, DEPName, &jsonProf)
if err != nil {
return ctxerr.Wrap(ctx, err, "apple POST /profile request failed")
}
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 {
var tmID *uint
if team != nil {
tmID = &team.ID
}
if err := d.ds.SetMDMAppleDefaultSetupAssistantProfileUUID(ctx, tmID, res.ProfileUUID); err != nil {
return ctxerr.Wrap(ctx, err, "save default setup assistant profile UUID")
}
}
return nil
}
// EnsureDefaultSetupAssistant ensures that the default Setup Assistant profile
// is created and registered with Apple for the provided team/no-team (if team
// is nil), and returns its profile UUID. It does not re-define the profile if
// it already exists and registered.
func (d *DEPService) EnsureDefaultSetupAssistant(ctx context.Context, team *fleet.Team) (string, time.Time, error) {
// the first step is to ensure that the default profile entry exists in the
// mdm_apple_enrollment_profiles table. When we create it there we also
// create the authentication token to retrieve enrollment profiles, and
// that's the place the token is stored.
defProf, err := d.ds.GetMDMAppleEnrollmentProfileByType(ctx, fleet.MDMAppleEnrollmentTypeAutomatic)
if err != nil && !fleet.IsNotFound(err) {
return "", time.Time{}, ctxerr.Wrap(ctx, err, "get default automatic profile")
}
if defProf == nil || defProf.Token == "" {
if err := d.createDefaultAutomaticProfile(ctx); err != nil {
return "", time.Time{}, ctxerr.Wrap(ctx, err, "create default automatic profile")
}
}
// now that the default automatic profile is created and a token generated,
// check if the default profile was registered with Apple for that team.
var tmID *uint
if team != nil {
tmID = &team.ID
}
profUUID, modTime, err := d.ds.GetMDMAppleDefaultSetupAssistant(ctx, tmID)
if err != nil && !fleet.IsNotFound(err) {
return "", time.Time{}, ctxerr.Wrap(ctx, err, "get default setup assistant profile uuid")
}
if profUUID == "" {
d.logger.Log("msg", "default DEP profile not set, registering")
if err := d.RegisterProfileWithAppleDEPServer(ctx, team, nil); err != nil {
return "", time.Time{}, ctxerr.Wrap(ctx, err, "register default setup assistant with Apple")
}
profUUID, modTime, err = d.ds.GetMDMAppleDefaultSetupAssistant(ctx, tmID)
if err != nil {
return "", time.Time{}, ctxerr.Wrap(ctx, err, "get default setup assistant profile uuid after registering")
}
}
return profUUID, modTime, nil
}
// EnsureCustomSetupAssistantIfExists ensures that the custom Setup Assistant
// profile associated with the provided team (or no team) is registered with
// Apple, and returns its profile UUID. It does not re-define the profile if it
// is already registered. If no custom setup assistant exists, it returns an
// empty string and timestamp and no error.
func (d *DEPService) EnsureCustomSetupAssistantIfExists(ctx context.Context, team *fleet.Team) (string, time.Time, error) {
var tmID *uint
if team != nil {
tmID = &team.ID
}
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
}
if asst.ProfileUUID == "" {
if err := d.RegisterProfileWithAppleDEPServer(ctx, team, asst); err != nil {
return "", time.Time{}, err
}
}
return asst.ProfileUUID, asst.UploadedAt, nil
}
func (d *DEPService) RunAssigner(ctx context.Context) error {
// get the Apple BM default team
appCfg, err := d.ds.AppConfig(ctx)
if err != nil {
return err
}
var appleBMTeam *fleet.Team
if appCfg.MDM.AppleBMDefaultTeam != "" {
tm, err := d.ds.TeamByName(ctx, appCfg.MDM.AppleBMDefaultTeam)
if err != nil && !fleet.IsNotFound(err) {
return err
}
appleBMTeam = tm
}
// ensure the default (fallback) setup assistant profile exists, registered
// with Apple DEP.
_, defModTime, err := d.EnsureDefaultSetupAssistant(ctx, appleBMTeam)
if err != nil {
return err
}
// if the team/no-team has a custom setup assistant, ensure it is registered
// with Apple DEP.
customUUID, customModTime, err := d.EnsureCustomSetupAssistantIfExists(ctx, appleBMTeam)
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 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 != "" && effectiveProfModTime.After(cursorModTime) {
d.logger.Log("msg", "clearing device syncer cursor")
if err := d.depStorage.StoreCursor(ctx, DEPName, ""); err != nil {
return err
}
}
return d.syncer.Run(ctx)
}
func NewDEPService(
ds fleet.Datastore,
depStorage nanodep_storage.AllStorage,
logger kitlog.Logger,
) *DEPService {
depClient := NewDEPClient(depStorage, ds, logger)
depSvc := &DEPService{
depStorage: depStorage,
logger: logger,
ds: ds,
}
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 {
return depSvc.processDeviceResponse(ctx, depClient, resp)
}),
)
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) error {
if len(resp.Devices) < 1 {
// no devices means we can't assign anything
return nil
}
var addedDevices []godep.Device
var deletedSerials []string
var modifiedDevices []godep.Device
var modifiedSerials []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,
)
switch strings.ToLower(device.OpType) {
// 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.
case "added", "":
addedDevices = append(addedDevices, device)
case "modified":
modifiedDevices = append(modifiedDevices, device)
modifiedSerials = append(modifiedSerials, device.SerialNumber)
case "deleted":
deletedSerials = append(deletedSerials, device.SerialNumber)
default:
level.Warn(d.logger).Log(
"msg", "unrecognized op_type",
"op_type", device.OpType,
"serial_number", device.SerialNumber,
)
}
}
existingSerials, err := d.ds.GetMatchingHostSerials(ctx, modifiedSerials)
if err != nil {
return ctxerr.Wrap(ctx, err, "get matching host serials")
}
// treat device that's coming as "modified" but doesn't exist in the
// `hosts` table, as an "added" device.
for _, d := range modifiedDevices {
if _, ok := existingSerials[d.SerialNumber]; !ok {
addedDevices = append(addedDevices, d)
}
}
err = d.ds.DeleteHostDEPAssignments(ctx, deletedSerials)
if err != nil {
return ctxerr.Wrap(ctx, err, "deleting DEP assignments")
}
n, defaultABMTeamID, err := d.ds.IngestMDMAppleDevicesFromDEPSync(ctx, addedDevices)
switch {
case err != nil:
level.Error(kitlog.With(d.logger)).Log("err", err)
sentry.CaptureException(err)
case n > 0:
level.Info(kitlog.With(d.logger)).Log("msg", fmt.Sprintf("added %d new mdm device(s) to pending hosts", n))
case n == 0:
level.Info(kitlog.With(d.logger)).Log("msg", "no DEP hosts to add")
}
// 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.
//
// collect a map of all the profiles => serials we need to assign.
profileToSerials := map[string][]string{}
// each new device should be assigned the DEP profile of the default
// ABM team as configured by the IT admin.
if len(addedDevices) > 0 {
level.Info(kitlog.With(d.logger)).Log("msg", "gathering added serials to assign devices", "len", len(addedDevices))
profUUID, err := d.getProfileUUIDForTeam(ctx, defaultABMTeamID)
if err != nil {
return ctxerr.Wrapf(ctx, err, "getting profile for default team with id: %v", defaultABMTeamID)
}
var addedSerials []string
for _, d := range addedDevices {
addedSerials = append(addedSerials, d.SerialNumber)
}
profileToSerials[profUUID] = addedSerials
} else {
level.Info(kitlog.With(d.logger)).Log("msg", "no added devices to assign DEP profiles")
}
// for all other hosts we received, find out the right DEP profile to assign, based on the team.
if len(existingSerials) > 0 {
level.Info(kitlog.With(d.logger)).Log("msg", "gathering existing serials to assign devices", "len", len(existingSerials))
serialsByTeam := map[*uint][]string{}
hosts := []fleet.Host{}
for _, host := range existingSerials {
if serialsByTeam[host.TeamID] == nil {
serialsByTeam[host.TeamID] = []string{}
}
serialsByTeam[host.TeamID] = append(serialsByTeam[host.TeamID], host.HardwareSerial)
hosts = append(hosts, *host)
}
for team, serials := range serialsByTeam {
profUUID, err := d.getProfileUUIDForTeam(ctx, team)
if err != nil {
return ctxerr.Wrapf(ctx, err, "getting profile for team with id: %v", team)
}
if profileToSerials[profUUID] == nil {
profileToSerials[profUUID] = []string{}
}
profileToSerials[profUUID] = append(profileToSerials[profUUID], serials...)
}
if err := d.ds.UpsertMDMAppleHostDEPAssignments(ctx, hosts); err != nil {
return ctxerr.Wrap(ctx, err, "upserting dep assignment for existing device")
}
} else {
level.Info(kitlog.With(d.logger)).Log("msg", "no existing devices to assign DEP profiles")
}
for profUUID, serials := range profileToSerials {
logger := kitlog.With(d.logger, "profile_uuid", profUUID)
level.Info(logger).Log("msg", "calling DEP client to assign profile", "profile_uuid", profUUID)
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
}
func (d *DEPService) getProfileUUIDForTeam(ctx context.Context, tmID *uint) (string, error) {
var appleBMTeam *fleet.Team
if tmID != nil {
tm, err := d.ds.Team(ctx, *tmID)
if err != nil && !fleet.IsNotFound(err) {
return "", ctxerr.Wrap(ctx, err, "get team")
}
appleBMTeam = tm
}
// get profile uuid of team or default
profUUID, _, err := d.EnsureCustomSetupAssistantIfExists(ctx, appleBMTeam)
if err != nil {
return "", fmt.Errorf("ensure setup assistant for team %v: %w", tmID, err)
}
if profUUID == "" {
profUUID, _, err = d.EnsureDefaultSetupAssistant(ctx, appleBMTeam)
if err != nil {
return "", fmt.Errorf("ensure default setup assistant: %w", err)
}
}
return profUUID, 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
// storage that will flag the AppConfig's AppleBMTermsExpired field
// whenever the status of the terms changes.
func NewDEPClient(storage godep.ClientStorage, appCfgUpdater fleet.AppConfigUpdater, logger kitlog.Logger) *godep.Client {
return godep.NewClient(storage, fleethttp.NewClient(), godep.WithAfterHook(func(ctx context.Context, reqErr error) error {
// if the request failed due to terms not signed, or if it succeeded,
// update the app config flag accordingly. If it failed for any other
// reason, do not update the flag.
termsExpired := reqErr != nil && godep.IsTermsNotSigned(reqErr)
if reqErr == nil || termsExpired {
appCfg, err := appCfgUpdater.AppConfig(ctx)
if err != nil {
level.Error(logger).Log("msg", "Apple DEP client: failed to get app config", "err", err)
return reqErr
}
var mustSaveAppCfg bool
if termsExpired && !appCfg.MDM.AppleBMTermsExpired {
// flag the AppConfig that the terms have changed and must be accepted
appCfg.MDM.AppleBMTermsExpired = true
mustSaveAppCfg = true
} else if reqErr == nil && appCfg.MDM.AppleBMTermsExpired {
// flag the AppConfig that the terms have been accepted
appCfg.MDM.AppleBMTermsExpired = false
mustSaveAppCfg = true
}
if mustSaveAppCfg {
if err := appCfgUpdater.SaveAppConfig(ctx, appCfg); err != nil {
level.Error(logger).Log("msg", "Apple DEP client: failed to save app config", "err", err)
}
level.Info(logger).Log("msg", "Apple DEP client: updated app config Terms Expired flag",
"apple_bm_terms_expired", appCfg.MDM.AppleBMTermsExpired)
}
}
return reqErr
}))
}
// enrollmentProfileMobileconfigTemplate is the template Fleet uses to assemble a .mobileconfig enrollment profile to serve to devices.
//
// During a profile replacement, the system updates payloads with the same PayloadIdentifier and
// PayloadUUID in the old and new profiles.
var enrollmentProfileMobileconfigTemplate = template.Must(template.New("").Parse(`
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PayloadContent</key>
<array>
<dict>
<key>PayloadContent</key>
<dict>
<key>Key Type</key>
<string>RSA</string>
<key>Challenge</key>
<string>{{ .SCEPChallenge }}</string>
<key>Key Usage</key>
<integer>5</integer>
<key>Keysize</key>
<integer>2048</integer>
<key>URL</key>
<string>{{ .SCEPURL }}</string>
<key>Subject</key>
<array>
<array><array><string>O</string><string>FleetDM</string></array></array>
<array><array><string>CN</string><string>FleetDM Identity</string></array></array>
</array>
</dict>
<key>PayloadIdentifier</key>
<string>com.fleetdm.fleet.mdm.apple.scep</string>
<key>PayloadType</key>
<string>com.apple.security.scep</string>
<key>PayloadUUID</key>
<string>BCA53F9D-5DD2-494D-98D3-0D0F20FF6BA1</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
<dict>
<key>AccessRights</key>
<integer>8191</integer>
<key>CheckOutWhenRemoved</key>
<true/>
<key>IdentityCertificateUUID</key>
<string>BCA53F9D-5DD2-494D-98D3-0D0F20FF6BA1</string>
<key>PayloadIdentifier</key>
<string>com.fleetdm.fleet.mdm.apple.mdm</string>
<key>PayloadType</key>
<string>com.apple.mdm</string>
<key>PayloadUUID</key>
<string>29713130-1602-4D27-90C9-B822A295E44E</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>ServerCapabilities</key>
<array>
<string>com.apple.mdm.per-user-connections</string>
<string>com.apple.mdm.bootstraptoken</string>
</array>
<key>ServerURL</key>
<string>{{ .ServerURL }}</string>
<key>SignMessage</key>
<true/>
<key>Topic</key>
<string>{{ .Topic }}</string>
</dict>
</array>
<key>PayloadDisplayName</key>
<string>{{ .Organization }} enrollment</string>
<key>PayloadIdentifier</key>
<string>` + FleetPayloadIdentifier + `</string>
<key>PayloadOrganization</key>
<string>{{ .Organization }}</string>
<key>PayloadScope</key>
<string>System</string>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>5ACABE91-CE30-4C05-93E3-B235C152404E</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>`))
func GenerateEnrollmentProfileMobileconfig(orgName, fleetURL, scepChallenge, topic string) ([]byte, error) {
scepURL, err := ResolveAppleSCEPURL(fleetURL)
if err != nil {
return nil, fmt.Errorf("resolve Apple SCEP url: %w", err)
}
serverURL, err := ResolveAppleMDMURL(fleetURL)
if err != nil {
return nil, fmt.Errorf("resolve Apple MDM url: %w", err)
}
var escaped strings.Builder
if err := xml.EscapeText(&escaped, []byte(scepChallenge)); err != nil {
return nil, fmt.Errorf("escape SCEP challenge for XML: %w", err)
}
var buf bytes.Buffer
if err := enrollmentProfileMobileconfigTemplate.Execute(&buf, struct {
Organization string
SCEPURL string
SCEPChallenge string
Topic string
ServerURL string
}{
Organization: orgName,
SCEPURL: scepURL,
SCEPChallenge: escaped.String(),
Topic: topic,
ServerURL: serverURL,
}); err != nil {
return nil, fmt.Errorf("execute template: %w", err)
}
return buf.Bytes(), nil
}
// ProfileBimap implements bidirectional mapping for profiles, and utility
// functions to generate those mappings based on frequently used operations.
type ProfileBimap struct {
wantedState map[*fleet.MDMAppleProfilePayload]*fleet.MDMAppleProfilePayload
currentState map[*fleet.MDMAppleProfilePayload]*fleet.MDMAppleProfilePayload
}
// NewProfileBimap retuns a new ProfileBimap
func NewProfileBimap() *ProfileBimap {
return &ProfileBimap{
map[*fleet.MDMAppleProfilePayload]*fleet.MDMAppleProfilePayload{},
map[*fleet.MDMAppleProfilePayload]*fleet.MDMAppleProfilePayload{},
}
}
// GetMatchingProfileInDesiredState returns the addition key that matches the given removal
func (pb *ProfileBimap) GetMatchingProfileInDesiredState(removal *fleet.MDMAppleProfilePayload) (*fleet.MDMAppleProfilePayload, bool) {
value, ok := pb.currentState[removal]
return value, ok
}
// GetMatchingProfileInCurrentState returns the removal key that matches the given addition
func (pb *ProfileBimap) GetMatchingProfileInCurrentState(addition *fleet.MDMAppleProfilePayload) (*fleet.MDMAppleProfilePayload, bool) {
key, ok := pb.wantedState[addition]
return key, ok
}
// IntersectByIdentifierAndHostUUID populates the bimap matching the profiles by Identifier and HostUUID
func (pb *ProfileBimap) IntersectByIdentifierAndHostUUID(wantedProfiles, currentProfiles []*fleet.MDMAppleProfilePayload) {
key := func(p *fleet.MDMAppleProfilePayload) string {
return fmt.Sprintf("%s-%s", p.ProfileIdentifier, p.HostUUID)
}
removeProfs := map[string]*fleet.MDMAppleProfilePayload{}
for _, p := range currentProfiles {
removeProfs[key(p)] = p
}
for _, p := range wantedProfiles {
if pp, ok := removeProfs[key(p)]; ok {
pb.add(p, pp)
}
}
}
func (pb *ProfileBimap) add(wantedProfile, currentProfile *fleet.MDMAppleProfilePayload) {
pb.wantedState[wantedProfile] = currentProfile
pb.currentState[currentProfile] = wantedProfile
}