29867 Block profile PayloadScope changes (#30429)

For #29867 . Includes latest copy requested by product.

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [x] Added/updated automated tests
- [x] Manual QA for all new/changed functionality
This commit is contained in:
Jordan Montgomery 2025-07-02 10:54:54 -04:00 committed by GitHub
parent 62da9b4149
commit 5263e95067
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 411 additions and 71 deletions

View file

@ -0,0 +1 @@
* Updated Apple profile verification code to disallow uploading profiles with the same identifier but differing PayloadScopes

View file

@ -37,7 +37,7 @@ const generateUnsupportedVariableErrMsg = (errMsg: string) => {
: DEFAULT_ERROR_MESSAGE;
};
const generateLearnMoreErrMsg = (errMsg: string, learnMoreUrl: string) => {
const generateSCEPLearnMoreErrMsg = (errMsg: string, learnMoreUrl: string) => {
return (
<>
Couldn&apos;t add. {errMsg}{" "}
@ -51,6 +51,29 @@ const generateLearnMoreErrMsg = (errMsg: string, learnMoreUrl: string) => {
);
};
const generateUserChannelLearnMoreErrMsg = (errMsg: string) => {
// The errors from the API for these errors contain couldn't add/couldn't edit
// depending on context so no need to include it here but we do want to remove
// the learn more link from the actual error since we will add a nicely formatted
// link to the error message.
if (errMsg.includes(" Learn more: https://")) {
errMsg = errMsg.substring(0, errMsg.indexOf(" Learn more: https://"));
}
return (
<>
{errMsg}{" "}
<CustomLink
url={
"https://fleetdm.com/learn-more-about/configuration-profiles-user-channel"
}
text="Learn more"
variant="flash-message-link"
newTab
/>
</>
);
};
/** We want to add some additional messageing to some of the error messages so
* we add them in this function. Otherwise, we'll just return the error message from the
* API.
@ -115,7 +138,7 @@ export const getErrorMessage = (err: AxiosResponse<IApiError>) => {
"can't be used if variables for SCEP URL and Challenge are not specified"
)
) {
return generateLearnMoreErrMsg(
return generateSCEPLearnMoreErrMsg(
apiReason,
"https://fleetdm.com/learn-more-about/certificate-authorities"
);
@ -126,7 +149,7 @@ export const getErrorMessage = (err: AxiosResponse<IApiError>) => {
"SCEP profile for custom SCEP certificate authority requires"
)
) {
return generateLearnMoreErrMsg(
return generateSCEPLearnMoreErrMsg(
apiReason,
"https://fleetdm.com/learn-more-about/custom-scep-configuration-profile"
);
@ -137,11 +160,15 @@ export const getErrorMessage = (err: AxiosResponse<IApiError>) => {
"SCEP profile for NDES certificate authority requires: $FLEET_VAR_NDES_SCEP_CHALLENGE"
)
) {
return generateLearnMoreErrMsg(
return generateSCEPLearnMoreErrMsg(
apiReason,
"https://fleetdm.com/learn-more-about/ndes-scep-configuration-profile"
);
}
if (apiReason.includes('"PayloadScope"')) {
return generateUserChannelLearnMoreErrMsg(apiReason);
}
return `${apiReason}` || DEFAULT_ERROR_MESSAGE;
};

View file

@ -5,6 +5,7 @@ import (
"context"
"crypto/aes"
"crypto/cipher"
"crypto/md5" // nolint:gosec // used only to hash for efficient comparisons
"crypto/rand"
"database/sql"
"encoding/hex"
@ -62,6 +63,122 @@ func isAppleHostConnectedToFleetMDM(ctx context.Context, q sqlx.QueryerContext,
return true, nil
}
// Checks scopes against existing profiles across the entire DB to ensure there are no conflicts
// where an existing profile with the same identifier has a different scope than the incoming
// profile. If we don't do this we must implement some sort of "move" semantics to allow for scope
// changes when a host switches teams or when a profile is updated.
func (ds *Datastore) verifyAppleConfigProfileScopesDoNotConflict(ctx context.Context, tx sqlx.ExtContext, cps []*fleet.MDMAppleConfigProfile) error {
if len(cps) == 0 {
return nil
}
incomingProfileIdentifiers := make([]string, 0, len(cps))
for i := 0; i < len(cps); i++ {
incomingProfileIdentifiers = append(incomingProfileIdentifiers, cps[i].Identifier)
}
stmt := `
SELECT
profile_uuid,
profile_id,
team_id,
name,
scope,
identifier,
mobileconfig,
created_at,
uploaded_at,
checksum
FROM
mdm_apple_configuration_profiles
WHERE
identifier IN (?)
`
stmt, args, err := sqlx.In(stmt, incomingProfileIdentifiers)
if err != nil {
return ctxerr.Wrap(ctx, err, "sqlx.In verifyAppleConfigProfileScopesDoNotConflict")
}
var existingProfiles []*fleet.MDMAppleConfigProfile
if err = sqlx.SelectContext(ctx, tx, &existingProfiles, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "querying existing apple config profiles by identifier")
}
existingProfilesByIdentifier := make(map[string][]*fleet.MDMAppleConfigProfile)
for _, existingProfile := range existingProfiles {
existingProfilesByIdentifier[existingProfile.Identifier] = append(existingProfilesByIdentifier[existingProfile.Identifier], existingProfile)
}
for _, cp := range cps {
existingProfiles := existingProfilesByIdentifier[cp.Identifier]
isEdit := false
scopeImplicitlyChanged := false
var conflictingProfile *fleet.MDMAppleConfigProfile
// We have to look through all profiles with the same identifier(which is potentially number
// of teams + 1, for no team) in most cases but even in the case of a large number of teams
for _, existingProfile := range existingProfiles {
var incomingProfileTeamID, existingProfileTeamID uint
if existingProfile.TeamID != nil {
existingProfileTeamID = *existingProfile.TeamID
}
if cp.TeamID != nil {
incomingProfileTeamID = *cp.TeamID
}
if incomingProfileTeamID == existingProfileTeamID {
isEdit = true
}
if existingProfile.Scope != cp.Scope {
if cp.Checksum == nil {
checksum := md5.Sum(cp.Mobileconfig) // nolint:gosec // Dismiss G401, we are not using this for secret/security reasons
cp.Checksum = checksum[:]
}
// If the existing profile is marked as system scope, the new profile is user scope
// but the checksums match, this is a profile that existed prior to User Channel
// support being added and is unmodified, so allow the existing behavior to continue
if existingProfile.Scope == fleet.PayloadScopeSystem && cp.Scope == fleet.PayloadScopeUser && bytes.Equal(existingProfile.Checksum, cp.Checksum) {
cp.Scope = existingProfile.Scope
conflictingProfile = nil
break
}
parsedConflictingMobileConfig, err := existingProfile.Mobileconfig.ParseConfigProfile()
if err != nil {
level.Debug(ds.logger).Log("msg", "error parsing existing profile mobileconfig while checking for scope conflicts",
"profile_uuid", existingProfile.ProfileUUID,
"err", err,
)
}
// The existing profile has a different scope in the XML than in Fleet's DB, meaning
// it existed prior to User channel profiles support being added and this is an
// implicit change the user may not be aware of.
if err == nil && fleet.PayloadScope(parsedConflictingMobileConfig.PayloadScope) != existingProfile.Scope {
scopeImplicitlyChanged = true
}
conflictingProfile = existingProfile
}
}
if conflictingProfile != nil {
var errorMessage string
// If you change this URL you may need to change the frontend code as well which adds a
// nicely formatted link to the error message.
const learnMoreUrl = "https://fleetdm.com/learn-more-about/configuration-profiles-user-channel"
if isEdit {
if scopeImplicitlyChanged {
errorMessage = fmt.Sprintf(`Couldn't edit configuration profile (%s) because it was previously delivered to some hosts on the device channel. Change "PayloadScope" to "System" to keep existing behavior. Alternatively, if you want this profile to be delivered on the user channel, please specify a new identifier for this profile and delete the old profile. Learn more: %s`, cp.Identifier, learnMoreUrl)
} else {
errorMessage = fmt.Sprintf(`Couldn't edit configuration profile (%s) because the profile's "PayloadScope" has changed. To change the “PayloadScope” of an existing profile, add a new profile with a new identifier with the desired scope and delete the old profile. Learn more: %s`, cp.Identifier, learnMoreUrl)
}
} else {
errorMessage = fmt.Sprintf(`Couldn't add configuration profile (%s) because "PayloadScope" conflicts with another profile with same identifier on a different team. Please use different identifier and try again. Learn more: %s`, cp.Identifier, learnMoreUrl)
}
return &fleet.BadRequestError{Message: errorMessage}
}
}
return nil
}
func (ds *Datastore) NewMDMAppleConfigProfile(ctx context.Context, cp fleet.MDMAppleConfigProfile, usesFleetVars []string) (*fleet.MDMAppleConfigProfile, error) {
profUUID := "a" + uuid.New().String()
stmt := `
@ -82,6 +199,10 @@ INSERT INTO
var profileID int64
err := ds.withTx(ctx, func(tx sqlx.ExtContext) error {
err := ds.verifyAppleConfigProfileScopesDoNotConflict(ctx, tx, []*fleet.MDMAppleConfigProfile{&cp})
if err != nil {
return err
}
res, err := tx.ExecContext(ctx, stmt,
profUUID, teamID, cp.Identifier, cp.Name, cp.Scope, cp.Mobileconfig, cp.Mobileconfig, cp.SecretsUpdatedAt, cp.Name, teamID, cp.Name,
teamID)
@ -2067,8 +2188,9 @@ func (ds *Datastore) GetNanoMDMEnrollment(ctx context.Context, id string) (*flee
func (ds *Datastore) GetNanoMDMUserEnrollment(ctx context.Context, deviceId string) (*fleet.NanoEnrollment, error) {
var nanoEnroll fleet.NanoEnrollment
// use writer as it is used just after creation in some cases
// Note that we only ever return the first active user enrollment from the device
err := sqlx.GetContext(ctx, ds.writer(ctx), &nanoEnroll, `SELECT id, device_id, type, enabled, token_update_tally
FROM nano_enrollments WHERE type = 'User' AND enabled = 1 AND device_id = ? LIMIT 1`, deviceId)
FROM nano_enrollments WHERE type = 'User' AND enabled = 1 AND device_id = ? ORDER BY created_at ASC LIMIT 1`, deviceId)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
@ -2081,6 +2203,7 @@ func (ds *Datastore) GetNanoMDMUserEnrollment(ctx context.Context, deviceId stri
func (ds *Datastore) GetNanoMDMUserEnrollmentUsername(ctx context.Context, deviceID string) (string, error) {
var username string
// Note that we only ever return the first active user enrollment from the device
err := sqlx.GetContext(ctx, ds.reader(ctx), &username, `
SELECT
COALESCE(nu.user_short_name, '') as user_short_name
@ -2091,6 +2214,7 @@ func (ds *Datastore) GetNanoMDMUserEnrollmentUsername(ctx context.Context, devic
ne.type = 'User' AND
ne.enabled = 1 AND
ne.device_id = ?
ORDER BY ne.created_at ASC
LIMIT 1`, deviceID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
@ -2161,7 +2285,6 @@ VALUES
ON DUPLICATE KEY UPDATE
uploaded_at = IF(checksum = VALUES(checksum) AND name = VALUES(name), uploaded_at, CURRENT_TIMESTAMP(6)),
secrets_updated_at = VALUES(secrets_updated_at),
scope = VALUES(scope),
checksum = VALUES(checksum),
name = VALUES(name),
mobileconfig = VALUES(mobileconfig)
@ -2173,6 +2296,11 @@ ON DUPLICATE KEY UPDATE
profTeamID = *tmID
}
err = ds.verifyAppleConfigProfileScopesDoNotConflict(ctx, tx, profiles)
if err != nil {
return false, err
}
// build a list of identifiers for the incoming profiles, will keep the
// existing ones if there's a match and no change
incomingIdents := make([]string, len(profiles))
@ -3763,6 +3891,9 @@ WHERE
// profiles (called in service.ensureFleetProfiles with the Fleet configuration
// profiles). Those are known not to use any Fleet variables. This method must
// not be used for any profile that uses Fleet variables.
// Also note this method does not implement the "do not allow profile scope to change"
// logic that other methods do and if it is ever used for non-fleet-controlled profiles
// it will need to.
func (ds *Datastore) BulkUpsertMDMAppleConfigProfiles(ctx context.Context, payload []*fleet.MDMAppleConfigProfile) error {
if len(payload) == 0 {
return nil
@ -3788,7 +3919,6 @@ func (ds *Datastore) BulkUpsertMDMAppleConfigProfiles(ctx context.Context, paylo
ON DUPLICATE KEY UPDATE
uploaded_at = IF(checksum = VALUES(checksum) AND name = VALUES(name), uploaded_at, CURRENT_TIMESTAMP()),
mobileconfig = VALUES(mobileconfig),
scope = VALUES(scope),
checksum = VALUES(checksum),
secrets_updated_at = VALUES(secrets_updated_at)
`, strings.TrimSuffix(sb.String(), ","))

View file

@ -4234,6 +4234,7 @@ func ReconcileAppleProfiles(
// and the checksums match (the profiles are exactly
// the same) we don't send another InstallProfile
// command.
if pp.Status != &fleet.MDMDeliveryFailed && bytes.Equal(pp.Checksum, p.Checksum) {
hostProfile := &fleet.MDMAppleBulkUpsertHostProfilePayload{
ProfileUUID: p.ProfileUUID,
@ -4288,25 +4289,36 @@ func ReconcileAppleProfiles(
installTargets[p.ProfileUUID] = target
}
sentToUserChannel := false
if p.Scope == fleet.PayloadScopeUser {
userEnrollmentID, err := getHostUserEnrollmentID(p.HostUUID)
if err != nil {
return err
}
if userEnrollmentID == "" {
level.Warn(logger).Log("msg", "host does not have a user enrollment, falling back to system enrollment for user scoped profile",
level.Warn(logger).Log("msg", "host does not have a user enrollment, failing profile installation",
"host_uuid", p.HostUUID, "profile_uuid", p.ProfileUUID, "profile_identifier", p.ProfileIdentifier)
} else {
sentToUserChannel = true
target.enrollmentIDs = append(target.enrollmentIDs, userEnrollmentID)
hostProfile := &fleet.MDMAppleBulkUpsertHostProfilePayload{
ProfileUUID: p.ProfileUUID,
HostUUID: p.HostUUID,
OperationType: fleet.MDMOperationTypeInstall,
Status: &fleet.MDMDeliveryFailed,
Detail: "This setting couldn't be enforced because the user channel doesn't exist for this host. Currently, Fleet creates the user channel for hosts that automatically enroll.",
CommandUUID: "",
ProfileIdentifier: p.ProfileIdentifier,
ProfileName: p.ProfileName,
Checksum: p.Checksum,
SecretsUpdatedAt: p.SecretsUpdatedAt,
Scope: p.Scope,
}
hostProfiles = append(hostProfiles, hostProfile)
continue
}
}
if !sentToUserChannel {
p.Scope = fleet.PayloadScopeSystem
target.enrollmentIDs = append(target.enrollmentIDs, userEnrollmentID)
} else {
target.enrollmentIDs = append(target.enrollmentIDs, p.HostUUID)
}
toGetContents[p.ProfileUUID] = true
hostProfile := &fleet.MDMAppleBulkUpsertHostProfilePayload{
ProfileUUID: p.ProfileUUID,
@ -4362,8 +4374,10 @@ func ReconcileAppleProfiles(
if userEnrollmentID == "" {
level.Warn(logger).Log("msg", "host does not have a user enrollment, cannot remove user scoped profile",
"host_uuid", p.HostUUID, "profile_uuid", p.ProfileUUID, "profile_identifier", p.ProfileIdentifier)
hostProfilesToCleanup = append(hostProfilesToCleanup, p)
continue
}
target.enrollmentIDs = append(target.enrollmentIDs, userEnrollmentID)
} else {
target.enrollmentIDs = append(target.enrollmentIDs, p.HostUUID)
@ -4393,11 +4407,16 @@ func ReconcileAppleProfiles(
// Create a map of command UUIDs to host IDs
commandUUIDToHostIDsCleanupMap := make(map[string][]string)
for _, hp := range hostProfilesToCleanup {
commandUUIDToHostIDsCleanupMap[hp.CommandUUID] = append(commandUUIDToHostIDsCleanupMap[hp.CommandUUID], hp.HostUUID)
// Certain failure scenarios may leave the profile without a command UUID, so skip those
if hp.CommandUUID != "" {
commandUUIDToHostIDsCleanupMap[hp.CommandUUID] = append(commandUUIDToHostIDsCleanupMap[hp.CommandUUID], hp.HostUUID)
}
}
// We need to delete commands from the nano queue so they don't get sent to device.
if err := commander.BulkDeleteHostUserCommandsWithoutResults(ctx, commandUUIDToHostIDsCleanupMap); err != nil {
return ctxerr.Wrap(ctx, err, "deleting nano commands without results")
if len(commandUUIDToHostIDsCleanupMap) > 0 {
if err := commander.BulkDeleteHostUserCommandsWithoutResults(ctx, commandUUIDToHostIDsCleanupMap); err != nil {
return ctxerr.Wrap(ctx, err, "deleting nano commands without results")
}
}
if err := ds.BulkDeleteMDMAppleHostsConfigProfiles(ctx, hostProfilesToCleanup); err != nil {
return ctxerr.Wrap(ctx, err, "deleting profiles that didn't change")

View file

@ -2370,7 +2370,7 @@ func TestMDMAppleReconcileAppleProfiles(t *testing.T) {
}
ds.BulkDeleteMDMAppleHostsConfigProfilesFunc = func(ctx context.Context, payload []*fleet.MDMAppleProfilePayload) error {
require.Empty(t, payload)
require.ElementsMatch(t, payload, []*fleet.MDMAppleProfilePayload{{ProfileUUID: p6, ProfileIdentifier: "com.remove.profile.six", HostUUID: hostUUID2, Scope: fleet.PayloadScopeUser}})
return nil
}
@ -2493,6 +2493,17 @@ func TestMDMAppleReconcileAppleProfiles(t *testing.T) {
cmdUUIDByProfileUUIDRemove := make(map[string]string)
copies := make([]*fleet.MDMAppleBulkUpsertHostProfilePayload, len(payload))
for i, p := range payload {
// clear the command UUID (in a copy so that it does not affect the
// pointed-to struct) from the payload for the subsequent checks
copyp := *p
copyp.CommandUUID = ""
copies[i] = &copyp
// Host with no user enrollment, so install fails
if p.HostUUID == hostUUID2 && p.ProfileUUID == p5 {
continue
}
if p.OperationType == fleet.MDMOperationTypeInstall {
existing, ok := cmdUUIDByProfileUUIDInstall[p.ProfileUUID]
if ok {
@ -2510,11 +2521,6 @@ func TestMDMAppleReconcileAppleProfiles(t *testing.T) {
}
}
// clear the command UUID (in a copy so that it does not affect the
// pointed-to struct) from the payload for the subsequent checks
copyp := *p
copyp.CommandUUID = ""
copies[i] = &copyp
}
require.ElementsMatch(t, []*fleet.MDMAppleBulkUpsertHostProfilePayload{
@ -2575,14 +2581,15 @@ func TestMDMAppleReconcileAppleProfiles(t *testing.T) {
Status: &fleet.MDMDeliveryPending,
Scope: fleet.PayloadScopeUser,
},
// This host has no user enrollment so the profile is sent to the device enrollment
// This host has no user enrollment so the profile is errored
{
ProfileUUID: p5,
ProfileIdentifier: "com.add.profile.five",
HostUUID: hostUUID2,
OperationType: fleet.MDMOperationTypeInstall,
Status: &fleet.MDMDeliveryPending,
Scope: fleet.PayloadScopeSystem,
Detail: "This setting couldn't be enforced because the user channel doesn't exist for this host. Currently, Fleet creates the user channel for hosts that automatically enroll.",
Status: &fleet.MDMDeliveryFailed,
Scope: fleet.PayloadScopeUser,
},
// This host has a user enrollment so the profile is removed from it
{
@ -2593,6 +2600,8 @@ func TestMDMAppleReconcileAppleProfiles(t *testing.T) {
Status: &fleet.MDMDeliveryPending,
Scope: fleet.PayloadScopeUser,
},
// Note that host2 has no user enrollment so the profile is not marked for removal
// from it
}, copies)
return nil
}
@ -2692,7 +2701,7 @@ func TestMDMAppleReconcileAppleProfiles(t *testing.T) {
failedCheck = func(payload []*fleet.MDMAppleBulkUpsertHostProfilePayload) {
failedCount++
require.Len(t, payload, 6) // the 6 install ops
require.Len(t, payload, 5) // the 5 install ops
require.ElementsMatch(t, []*fleet.MDMAppleBulkUpsertHostProfilePayload{
{
ProfileUUID: p1,
@ -2737,15 +2746,6 @@ func TestMDMAppleReconcileAppleProfiles(t *testing.T) {
CommandUUID: "",
Scope: fleet.PayloadScopeUser,
},
{
ProfileUUID: p5,
ProfileIdentifier: "com.add.profile.five",
HostUUID: hostUUID2,
OperationType: fleet.MDMOperationTypeInstall,
Status: nil,
CommandUUID: "",
Scope: fleet.PayloadScopeSystem,
},
}, payload)
}
@ -2764,6 +2764,10 @@ func TestMDMAppleReconcileAppleProfiles(t *testing.T) {
ds.ListMDMAppleProfilesToRemoveFunc = func(ctx context.Context) ([]*fleet.MDMAppleProfilePayload, error) {
return nil, nil
}
ds.BulkDeleteMDMAppleHostsConfigProfilesFunc = func(ctx context.Context, payload []*fleet.MDMAppleProfilePayload) error {
require.Empty(t, payload)
return nil
}
ds.BulkUpsertMDMAppleHostProfilesFunc = func(ctx context.Context, payload []*fleet.MDMAppleBulkUpsertHostProfilePayload) error {
if failedCall {
failedCheck(payload)
@ -2779,6 +2783,17 @@ func TestMDMAppleReconcileAppleProfiles(t *testing.T) {
cmdUUIDByProfileUUIDRemove := make(map[string]string)
copies := make([]*fleet.MDMAppleBulkUpsertHostProfilePayload, len(payload))
for i, p := range payload {
// clear the command UUID (in a copy so that it does not affect the
// pointed-to struct) from the payload for the subsequent checks
copyp := *p
copyp.CommandUUID = ""
copies[i] = &copyp
// Host with no user enrollment, so install fails
if p.HostUUID == hostUUID2 && p.ProfileUUID == p5 {
continue
}
if p.OperationType == fleet.MDMOperationTypeInstall {
existing, ok := cmdUUIDByProfileUUIDInstall[p.ProfileUUID]
if ok {
@ -2795,12 +2810,6 @@ func TestMDMAppleReconcileAppleProfiles(t *testing.T) {
cmdUUIDByProfileUUIDRemove[p.ProfileUUID] = p.CommandUUID
}
}
// clear the command UUID (in a copy so that it does not affect the
// pointed-to struct) from the payload for the subsequent checks
copyp := *p
copyp.CommandUUID = ""
copies[i] = &copyp
}
require.ElementsMatch(t, []*fleet.MDMAppleBulkUpsertHostProfilePayload{
@ -2850,8 +2859,9 @@ func TestMDMAppleReconcileAppleProfiles(t *testing.T) {
ProfileIdentifier: "com.add.profile.five",
HostUUID: hostUUID2,
OperationType: fleet.MDMOperationTypeInstall,
Status: &fleet.MDMDeliveryPending,
Scope: fleet.PayloadScopeSystem,
Status: &fleet.MDMDeliveryFailed,
Detail: "This setting couldn't be enforced because the user channel doesn't exist for this host. Currently, Fleet creates the user channel for hosts that automatically enroll.",
Scope: fleet.PayloadScopeUser,
},
}, copies)
return nil
@ -2971,6 +2981,10 @@ func TestMDMAppleReconcileAppleProfiles(t *testing.T) {
profilesToInstall, _ := ds.ListMDMAppleProfilesToInstallFunc(ctx)
hostUUIDs = make([]string, 0, len(profilesToInstall))
for _, p := range profilesToInstall {
// This host will error before this point - should not be updated by the variable failure
if p.HostUUID == hostUUID2 && p.ProfileUUID == p5 {
continue
}
hostUUIDs = append(hostUUIDs, p.HostUUID)
}
@ -3721,7 +3735,9 @@ func TestMDMApplePreassignEndpoints(t *testing.T) {
}
// Helper for creating scoped mobileconfigs. scope is optional and if set to nil is not included in
// the mobileconfig so that default behavior is used.
// the mobileconfig so that default behavior is used. Note that because Fleet enforces that all
// profiles sharing a given identifier have the same scope, it's a good idea to use a unique
// identifier in your test or perhaps one with the scope in its name
func scopedMobileconfigForTest(name, identifier string, scope *fleet.PayloadScope, vars ...string) []byte {
var varsStr strings.Builder
for i, v := range vars {

View file

@ -2458,7 +2458,7 @@ func (s *integrationMDMTestSuite) TestHostMDMAppleProfilesStatus() {
globalProfiles := [][]byte{
mobileconfigForTest("G1", "G1"),
scopedMobileconfigForTest("G2", "G2", &payloadScopeSystem),
scopedMobileconfigForTest("G3", "G3", &payloadScopeUser),
scopedMobileconfigForTest("G3", "G3.user", &payloadScopeUser),
}
s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch",
batchSetMDMAppleProfilesRequest{Profiles: globalProfiles}, http.StatusNoContent)
@ -2478,7 +2478,7 @@ func (s *integrationMDMTestSuite) TestHostMDMAppleProfilesStatus() {
tm1Profiles := [][]byte{
mobileconfigForTest("T1.1", "T1.1"),
scopedMobileconfigForTest("T1.2", "T1.2", &payloadScopeSystem),
scopedMobileconfigForTest("T1.3", "T1.3", &payloadScopeUser),
scopedMobileconfigForTest("T1.3", "T1.3.user", &payloadScopeUser),
}
s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch",
batchSetMDMAppleProfilesRequest{Profiles: tm1Profiles}, http.StatusNoContent,
@ -2510,20 +2510,20 @@ func (s *integrationMDMTestSuite) TestHostMDMAppleProfilesStatus() {
s.awaitTriggerProfileSchedule(t)
// G3 is user-scoped and the h2 host doesn't have a user-channel yet (and
// enrolled just now, so the minimum delay to give up and send the
// user-scoped profiles to the device channel is not reached)
// enrolled just now, so the minimum delay to give up and fail the profile
// delivery is not reached)
s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
h1: {
{Identifier: "G1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: "G2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: "G3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: "G3.user", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
},
h2: {
{Identifier: "G1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: "G2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: "G3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: "G3.user", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
},
@ -2554,14 +2554,14 @@ func (s *integrationMDMTestSuite) TestHostMDMAppleProfilesStatus() {
h3: {
{Identifier: "T1.1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: "T1.2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: "T1.3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: "T1.3.user", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
},
h4: {
{Identifier: "T1.1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: "T1.2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: "T1.3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: "T1.3.user", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
},
@ -2585,7 +2585,7 @@ func (s *integrationMDMTestSuite) TestHostMDMAppleProfilesStatus() {
h1: {
{Identifier: "G1", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
{Identifier: "G2", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
{Identifier: "G3", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
{Identifier: "G3.user", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
{Identifier: "T2.1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
@ -2593,7 +2593,7 @@ func (s *integrationMDMTestSuite) TestHostMDMAppleProfilesStatus() {
h2: {
{Identifier: "G1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: "G2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: "G3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: "G3.user", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
},
@ -2613,7 +2613,7 @@ func (s *integrationMDMTestSuite) TestHostMDMAppleProfilesStatus() {
h3: {
{Identifier: "T1.1", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
{Identifier: "T1.2", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
{Identifier: "T1.3", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
{Identifier: "T1.3.user", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
{Identifier: "T2.1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
@ -2621,7 +2621,7 @@ func (s *integrationMDMTestSuite) TestHostMDMAppleProfilesStatus() {
h4: {
{Identifier: "T1.1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: "T1.2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: "T1.3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: "T1.3.user", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
},
@ -2634,7 +2634,7 @@ func (s *integrationMDMTestSuite) TestHostMDMAppleProfilesStatus() {
h3: {
{Identifier: "T1.1", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
{Identifier: "T1.2", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
{Identifier: "T1.3", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
{Identifier: "T1.3.user", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
{Identifier: "T2.1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
@ -2642,10 +2642,10 @@ func (s *integrationMDMTestSuite) TestHostMDMAppleProfilesStatus() {
h4: {
{Identifier: "T1.1", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
{Identifier: "T1.2", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
{Identifier: "T1.3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending}, // still pending install due to cron not having run
{Identifier: "T1.3.user", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending}, // still pending install due to cron not having run
{Identifier: "G1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: "G2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: "G3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: "G3.user", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
},
@ -2662,7 +2662,7 @@ func (s *integrationMDMTestSuite) TestHostMDMAppleProfilesStatus() {
h2: { // still no user channel
{Identifier: "G1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: "G2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: "G3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: "G3.user", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: "G4", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
@ -2670,7 +2670,7 @@ func (s *integrationMDMTestSuite) TestHostMDMAppleProfilesStatus() {
h4: {
{Identifier: "G1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: "G2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: "G3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: "G3.user", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: "G4", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
@ -2717,7 +2717,7 @@ func (s *integrationMDMTestSuite) TestHostMDMAppleProfilesStatus() {
h2: {
{Identifier: "G1", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
{Identifier: "G2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: "G3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: "G3.user", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: "G4", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
@ -2725,7 +2725,7 @@ func (s *integrationMDMTestSuite) TestHostMDMAppleProfilesStatus() {
h4: {
{Identifier: "G1", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
{Identifier: "G2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: "G3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: "G3.user", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: "G4", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
@ -2788,7 +2788,7 @@ func (s *integrationMDMTestSuite) TestHostMDMAppleProfilesStatus() {
h4: {
{Identifier: "G2", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
{Identifier: "G2b", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: "G3", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
{Identifier: "G3.user", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
{Identifier: "G4", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
{Identifier: "G5", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
@ -2844,7 +2844,7 @@ func (s *integrationMDMTestSuite) TestHostMDMAppleProfilesStatus() {
s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/batch",
batchSetMDMAppleProfilesRequest{
Profiles: [][]byte{
mobileconfigForTest("T1.3", "T1.3"),
scopedMobileconfigForTest("T1.3", "T1.3.user", &payloadScopeUser),
},
}, http.StatusNoContent, "team_id", fmt.Sprint(tm1.ID))
s.assertHostAppleConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
@ -3011,7 +3011,7 @@ func (s *integrationMDMTestSuite) TestHostMDMAppleProfilesStatus() {
h1: {
{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: "T1.3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: "T1.3.user", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: "label_prof", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
},
h2: {
@ -3042,7 +3042,7 @@ func (s *integrationMDMTestSuite) TestHostMDMAppleProfilesStatus() {
h1: {
{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: "T1.3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: "T1.3.user", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
{Identifier: "label_prof", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerified},
},
h2: {
@ -6540,8 +6540,8 @@ func (s *integrationMDMTestSuite) TestVerifyUserScopedProfiles() {
payloadScopeUser := fleet.PayloadScopeUser
profiles := []fleet.MDMProfileBatchPayload{
{Name: "A1", Contents: scopedMobileconfigForTest("A1", "A1", &payloadScopeSystem)},
{Name: "A2", Contents: scopedMobileconfigForTest("A2", "A2", &payloadScopeUser)},
{Name: "A3", Contents: scopedMobileconfigForTest("A3", "A3", &payloadScopeUser)},
{Name: "A2", Contents: scopedMobileconfigForTest("A2", "A2.user", &payloadScopeUser)},
{Name: "A3", Contents: scopedMobileconfigForTest("A3", "A3.user", &payloadScopeUser)},
}
s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: profiles}, http.StatusNoContent)
@ -6858,3 +6858,150 @@ func (s *integrationMDMTestSuite) TestVerifyUserScopedProfiles() {
},
})
}
func (s *integrationMDMTestSuite) TestMDMAppleProfileScopeChanges() {
t := s.T()
ctx := context.Background()
// add a couple global profiles
payloadScopeSystem := fleet.PayloadScopeSystem
payloadScopeUser := fleet.PayloadScopeUser
globalProfiles := [][]byte{
mobileconfigForTest("G1", "G1"),
scopedMobileconfigForTest("G2", "G2", &payloadScopeSystem),
scopedMobileconfigForTest("G3", "G3.user", &payloadScopeUser),
scopedMobileconfigForTest("G4", "G4.user-but-actually-system", &payloadScopeUser),
}
s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch",
batchSetMDMAppleProfilesRequest{Profiles: globalProfiles}, http.StatusNoContent)
// Create a profile with a scope that is System in the DB but User in the XML. This mimics
// our upgrade behavior from versions prior to 4.71 to 4.71+ when we added support for User
// scoped profiles
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
stmt := `UPDATE mdm_apple_configuration_profiles SET scope=? WHERE identifier=?;`
_, err := q.ExecContext(context.Background(), stmt, fleet.PayloadScopeSystem, "G4.user-but-actually-system")
return err
})
// create a team with a couple profiles
tm1, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "team_profile_scope_changes_1"})
require.NoError(t, err)
tm1Profiles := [][]byte{
mobileconfigForTest("T1.1", "T1.1"),
scopedMobileconfigForTest("T1.2", "T1.2", &payloadScopeSystem),
scopedMobileconfigForTest("T1.3", "T1.3.user", &payloadScopeUser),
}
s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch",
batchSetMDMAppleProfilesRequest{Profiles: tm1Profiles}, http.StatusNoContent,
"team_id", fmt.Sprint(tm1.ID))
// create a second team with different profiles
tm2, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "team_profile_scope_changes_2"})
require.NoError(t, err)
tm2Profiles := [][]byte{
mobileconfigForTest("T2.1", "T2.1"),
scopedMobileconfigForTest("T2.2", "T2.2.user", &payloadScopeSystem),
scopedMobileconfigForTest("T2.3", "T2.3.user", &payloadScopeUser),
}
s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch",
batchSetMDMAppleProfilesRequest{Profiles: tm2Profiles}, http.StatusNoContent,
"team_id", fmt.Sprint(tm2.ID))
// Do a no-op update of each team's profiles, verify no errors are returned
s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch",
batchSetMDMAppleProfilesRequest{Profiles: globalProfiles}, http.StatusNoContent)
s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch",
batchSetMDMAppleProfilesRequest{Profiles: tm1Profiles}, http.StatusNoContent,
"team_id", fmt.Sprint(tm1.ID))
s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch",
batchSetMDMAppleProfilesRequest{Profiles: tm2Profiles}, http.StatusNoContent,
"team_id", fmt.Sprint(tm2.ID))
// Test a modification of an existing global profile with an implicit scope change
newGlobalProfiles := [][]byte{
globalProfiles[0],
globalProfiles[1],
globalProfiles[2],
scopedMobileconfigForTest("G4-bozo", "G4.user-but-actually-system", &payloadScopeUser),
}
response := s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch",
batchSetMDMAppleProfilesRequest{Profiles: newGlobalProfiles}, http.StatusBadRequest)
errMsg := extractServerErrorText(response.Body)
require.Contains(t, errMsg, "Couldn't edit configuration profile (G4.user-but-actually-system) because it was previously delivered to some hosts on the device channel")
// Test a conflict of a profile on a team with an existing global profile and an implicit scope change
// Should error because "G4.user-but-actually-system" conflicts with global
// "G4.user-but-actually-system" profile scope
newTm1Profiles := [][]byte{
tm1Profiles[0], // T1.1
tm1Profiles[1], // T1.2
tm1Profiles[2], // T1.3.user
scopedMobileconfigForTest("G4-bozo", "G4.user-but-actually-system", &payloadScopeUser),
}
response = s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch",
batchSetMDMAppleProfilesRequest{Profiles: newTm1Profiles}, http.StatusBadRequest,
"team_id", fmt.Sprint(tm1.ID))
errMsg = extractServerErrorText(response.Body)
require.Contains(t, errMsg, "Couldn't add configuration profile (G4.user-but-actually-system) because \"PayloadScope\" conflicts")
// Test a conflict of a profile on a team with an existing global profile
// Should error because "G2" conflicts with global "G2" profile
newTm1Profiles = [][]byte{
tm1Profiles[0], // T1.1
tm1Profiles[1], // T1.2
tm1Profiles[2], // T1.3.user
scopedMobileconfigForTest("G2", "G2", &payloadScopeUser),
}
response = s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch",
batchSetMDMAppleProfilesRequest{Profiles: newTm1Profiles}, http.StatusBadRequest,
"team_id", fmt.Sprint(tm1.ID))
errMsg = extractServerErrorText(response.Body)
require.Contains(t, errMsg, "Couldn't add configuration profile (G2) because \"PayloadScope\" conflicts")
// Test a conflict of a profile on a team versus one with the same identifier but different
// scope on a different team.
// Should error because "T2.3.user" system-scoped profile conflicts with team2 "T2.3.user" user-scoped profile
newTm1Profiles = [][]byte{
tm1Profiles[0], // T1.1
tm1Profiles[1], // T1.2
tm1Profiles[2], // T1.3.user
scopedMobileconfigForTest("T2.3", "T2.3.user", &payloadScopeSystem), // T2.3.user changed to system scope
}
response = s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch",
batchSetMDMAppleProfilesRequest{Profiles: newTm1Profiles}, http.StatusBadRequest,
"team_id", fmt.Sprint(tm1.ID))
errMsg = extractServerErrorText(response.Body)
require.Contains(t, errMsg, "Couldn't add configuration profile (T2.3.user) because \"PayloadScope\" conflicts")
// Profile edit of existing profile on team1 with a new scope
newTm1Profiles = [][]byte{
tm1Profiles[0], // T1.1
tm1Profiles[1], // T1.2
scopedMobileconfigForTest("T1.3", "T1.3.user", &payloadScopeSystem), // T1.3.user changed to system scope
}
response = s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch",
batchSetMDMAppleProfilesRequest{Profiles: newTm1Profiles}, http.StatusBadRequest,
"team_id", fmt.Sprint(tm1.ID))
errMsg = extractServerErrorText(response.Body)
require.Contains(t, errMsg, "Couldn't edit configuration profile (T1.3.user) because the profile's \"PayloadScope\" has changed")
// Should be able to add these profiles to team1 with the proper scopes
newTm1Profiles = [][]byte{
tm1Profiles[0], // T1.1
tm1Profiles[1], // T1.2
tm1Profiles[2], // T1.3.user
scopedMobileconfigForTest("G2", "G2", &payloadScopeSystem),
scopedMobileconfigForTest("T2.3", "T2.3.user", &payloadScopeUser), // T2.3.user changed to system scope
}
s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch",
batchSetMDMAppleProfilesRequest{Profiles: newTm1Profiles}, http.StatusNoContent,
"team_id", fmt.Sprint(tm1.ID))
}