Skip to content

Commit

Permalink
Add cross-platform check for duplicate MDM profile names (#17916)
Browse files Browse the repository at this point in the history
  • Loading branch information
gillespi314 authored Mar 29, 2024
1 parent 140dde1 commit 841350f
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 27 deletions.
1 change: 1 addition & 0 deletions changes/17559-batch-set-duplicate-mdm
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Added cross-platform check for duplicate MDM profiles names in batch set MDM profiles API.
29 changes: 28 additions & 1 deletion server/service/apple_mdm.go
Original file line number Diff line number Diff line change
Expand Up @@ -1752,6 +1752,34 @@ func (svc *Service) BatchSetMDMAppleProfiles(ctx context.Context, tmID *uint, tm
profs = append(profs, mdmProf)
}

if !skipBulkPending {
// check for duplicates with existing profiles, skipBulkPending signals that the caller
// is responsible for ensuring that the profiles names are unique (e.g., MDMAppleMatchPreassignment)
allProfs, _, err := svc.ds.ListMDMConfigProfiles(ctx, tmID, fleet.ListOptions{PerPage: 0})
if err != nil {
return ctxerr.Wrap(ctx, err, "list mdm config profiles")
}
for _, p := range allProfs {
if byName[p.Name] {
switch {
case strings.HasPrefix(p.ProfileUUID, "a"):
// do nothing, all existing mobileconfigs will be replaced and we've already checked
// the new mobileconfigs for duplicates
continue
case strings.HasPrefix(p.ProfileUUID, "w"):
err := fleet.NewInvalidArgumentError("PayloadDisplayName", fmt.Sprintf(
"Couldn’t edit custom_settings. A Windows configuration profile shares the same name as a macOS configuration profile (PayloadDisplayName): %q", p.Name))
return ctxerr.Wrap(ctx, err, "duplicate xml and mobileconfig by name")
default:
err := fleet.NewInvalidArgumentError("PayloadDisplayName", fmt.Sprintf(
"Couldn’t edit custom_settings. More than one configuration profile have the same name (PayloadDisplayName): %q", p.Name))
return ctxerr.Wrap(ctx, err, "duplicate json and mobileconfig by name")
}
}
byName[p.Name] = true
}
}

if dryRun {
return nil
}
Expand Down Expand Up @@ -2753,7 +2781,6 @@ func ReconcileAppleDeclarations(
commander *apple_mdm.MDMAppleCommander,
logger kitlog.Logger,
) error {

// batch set declarations as pending
changedHosts, err := ds.MDMAppleBatchSetHostDeclarationState(ctx)
if err != nil {
Expand Down
6 changes: 6 additions & 0 deletions server/service/apple_mdm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1430,6 +1430,9 @@ func TestMDMBatchSetAppleProfiles(t *testing.T) {
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string) error {
return nil
}
ds.ListMDMConfigProfilesFunc = func(ctx context.Context, tid *uint, opt fleet.ListOptions) ([]*fleet.MDMConfigProfilePayload, *fleet.PaginationMetadata, error) {
return nil, nil, nil
}

type testCase struct {
name string
Expand Down Expand Up @@ -1741,6 +1744,9 @@ func TestMDMBatchSetAppleProfilesBoolArgs(t *testing.T) {
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, profileUUIDs, uuids []string) error {
return nil
}
ds.ListMDMConfigProfilesFunc = func(ctx context.Context, tid *uint, opt fleet.ListOptions) ([]*fleet.MDMConfigProfilePayload, *fleet.PaginationMetadata, error) {
return nil, nil, nil
}

ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
ctx = license.NewContext(ctx, &fleet.LicenseInfo{Tier: fleet.TierPremium})
Expand Down
9 changes: 5 additions & 4 deletions server/service/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,8 @@ func (c *Client) runAppConfigChecks(fn func(ac *fleet.EnrichedAppConfig) error)
// getProfilesContents takes file paths and creates a slice of profile payloads
// ready to batch-apply.
func getProfilesContents(baseDir string, profiles []fleet.MDMProfileSpec) ([]fleet.MDMProfileBatchPayload, error) {
fileNameMap := make(map[string]struct{}, len(profiles))
// map to check for duplicate names
extByName := make(map[string]string, len(profiles))
result := make([]fleet.MDMProfileBatchPayload, 0, len(profiles))

for _, profile := range profiles {
Expand All @@ -306,10 +307,10 @@ func getProfilesContents(baseDir string, profiles []fleet.MDMProfileSpec) ([]fle
}
name = strings.TrimSpace(mc.Name)
}
if _, isDuplicate := fileNameMap[name]; isDuplicate {
return nil, errors.New("Couldn't edit windows_settings.custom_settings. More than one configuration profile have the same name (Windows .xml file name or macOS PayloadDisplayName).")
if e, isDuplicate := extByName[name]; isDuplicate {
return nil, errors.New(fmtDuplicateNameErrMsg(name, e, ext))
}
fileNameMap[name] = struct{}{}
extByName[name] = ext
result = append(result, fleet.MDMProfileBatchPayload{
Name: name,
Contents: fileContents,
Expand Down
3 changes: 1 addition & 2 deletions server/service/integration_ddm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func (s *integrationMDMTestSuite) TestAppleDDMBatchUpload() {
{Name: "bad2", Contents: newDeclBytes(2, `"baz": "bing"`)},
}}, http.StatusUnprocessableEntity)
errMsg = extractServerErrorText(res.Body)
require.Contains(t, errMsg, "A declaration profile with this name already exists.")
require.Contains(t, errMsg, "More than one configuration profile have the same name")

// Same identifier should fail
res = s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
Expand Down Expand Up @@ -877,7 +877,6 @@ func (s *integrationMDMTestSuite) TestDDMUnsupportedDevice() {
require.Contains(t, profs["I1"].Detail, "Feature Disabled")
require.Equal(t, &fleet.MDMDeliveryFailed, profs["I2"].Status)
require.Contains(t, profs["I2"].Detail, "Feature Disabled")

}

func (s *integrationMDMTestSuite) TestDDMNoDeclarationsLeft() {
Expand Down
39 changes: 39 additions & 0 deletions server/service/integration_mdm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10990,11 +10990,50 @@ func (s *integrationMDMTestSuite) TestBatchSetMDMProfiles() {
fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, tm.ID, tm.Name),
0,
)

s.lastActivityOfTypeMatches(
fleet.ActivityTypeEditedDeclarationProfile{}.ActivityName(),
fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, tm.ID, tm.Name),
0,
)

// names cannot be duplicated across platforms
declBytes := json.RawMessage(`{
"Type": "com.apple.configuration.decl.foo",
"Identifier": "com.fleet.config.foo",
"Payload": {
"ServiceType": "com.apple.bash",
"DataAssetReference": "com.fleet.asset.bash"
}}`)
mcBytes := mobileconfigForTest("N1", "I1")
winBytes := syncMLForTest("./Foo/Bar")

for _, p := range []struct {
payload []fleet.MDMProfileBatchPayload
expectErr string
}{
{
payload: []fleet.MDMProfileBatchPayload{{Name: "N1", Contents: mcBytes}, {Name: "N1", Contents: winBytes}},
expectErr: "More than one configuration profile have the same name 'N1' (Windows .xml file name or macOS .mobileconfig PayloadDisplayName).",
},
{
payload: []fleet.MDMProfileBatchPayload{{Name: "N1", Contents: declBytes}, {Name: "N1", Contents: winBytes}},
expectErr: "More than one configuration profile have the same name 'N1' (macOS .json file name or Windows .xml file name).",
},
{
payload: []fleet.MDMProfileBatchPayload{{Name: "N1", Contents: mcBytes}, {Name: "N1", Contents: declBytes}},
expectErr: "More than one configuration profile have the same name 'N1' (macOS .json file name or macOS .mobileconfig PayloadDisplayName).",
},
} {
// team profiles
res = s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: p.payload}, http.StatusUnprocessableEntity, "team_id", strconv.Itoa(int(tm.ID)))
errMsg = extractServerErrorText(res.Body)
require.Contains(t, errMsg, p.expectErr)
// no team profiles
res = s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: p.payload}, http.StatusUnprocessableEntity)
errMsg = extractServerErrorText(res.Body)
require.Contains(t, errMsg, p.expectErr)
}
}

func (s *integrationMDMTestSuite) TestBatchSetMDMProfilesBackwardsCompat() {
Expand Down
89 changes: 69 additions & 20 deletions server/service/mdm.go
Original file line number Diff line number Diff line change
Expand Up @@ -1527,6 +1527,10 @@ func (svc *Service) BatchSetMDMProfiles(
return ctxerr.Wrap(ctx, err, "validating Windows profiles")
}

if err := svc.validateCrossPlatformProfileNames(ctx, appleProfiles, windowsProfiles, appleDecls); err != nil {
return ctxerr.Wrap(ctx, err, "validating cross-platform profile names")
}

if dryRun {
return nil
}
Expand Down Expand Up @@ -1578,6 +1582,70 @@ func (svc *Service) BatchSetMDMProfiles(
return nil
}

func (svc *Service) validateCrossPlatformProfileNames(ctx context.Context, appleProfiles []*fleet.MDMAppleConfigProfile, windowsProfiles []*fleet.MDMWindowsConfigProfile, appleDecls []*fleet.MDMAppleDeclaration) error {
// map all profile names to check for duplicates, regardless of platform; key is name, value is one of
// ".mobileconfig" or ".json" or ".xml"
extByName := make(map[string]string, len(appleProfiles)+len(windowsProfiles)+len(appleDecls))
for i, p := range appleProfiles {
if v, ok := extByName[p.Name]; ok {
err := fleet.NewInvalidArgumentError(fmt.Sprintf("appleProfiles[%d]", i), fmtDuplicateNameErrMsg(p.Name, ".mobileconfig", v))
return ctxerr.Wrap(ctx, err, "duplicate mobileconfig profile by name")
}
extByName[p.Name] = ".mobileconfig"
}
for i, p := range windowsProfiles {
if v, ok := extByName[p.Name]; ok {
err := fleet.NewInvalidArgumentError(fmt.Sprintf("windowsProfiles[%d]", i), fmtDuplicateNameErrMsg(p.Name, ".xml", v))
return ctxerr.Wrap(ctx, err, "duplicate xml by name")
}
extByName[p.Name] = ".xml"
}
for i, p := range appleDecls {
if v, ok := extByName[p.Name]; ok {
err := fleet.NewInvalidArgumentError(fmt.Sprintf("appleDecls[%d]", i), fmtDuplicateNameErrMsg(p.Name, ".json", v))
return ctxerr.Wrap(ctx, err, "duplicate json by name")
}
extByName[p.Name] = ".json"
}

return nil
}

func fmtDuplicateNameErrMsg(name, fileType1, fileType2 string) string {
var part1 string
switch fileType1 {
case ".xml":
part1 = "Windows .xml file name"
case ".mobileconfig":
part1 = "macOS .mobileconfig PayloadDisplayName"
case ".json":
part1 = "macOS .json file name"
}

var part2 string
switch fileType2 {
case ".xml":
part2 = "Windows .xml file name"
case ".mobileconfig":
part2 = "macOS .mobileconfig PayloadDisplayName"
case ".json":
part2 = "macOS .json file name"
}

base := fmt.Sprintf(`Couldn’t edit custom_settings. More than one configuration profile have the same name '%s'`, name)
detail := ` (%s).`
switch {
case part1 == part2:
return fmt.Sprintf(base+detail, part1)
case part1 != "" && part2 != "":
return fmt.Sprintf(base+detail, fmt.Sprintf("%s or %s", part1, part2))
case part1 != "" || part2 != "":
return fmt.Sprintf(base+detail, part1+part2)
default:
return base + "." // should never happen
}
}

func (svc *Service) authorizeBatchProfiles(ctx context.Context, tmID *uint, tmName *string) (*uint, *string, error) {
if tmID != nil && tmName != nil {
svc.authz.SkipAuthorization(ctx) // so that the error message is not replaced by "forbidden"
Expand Down Expand Up @@ -1658,26 +1726,7 @@ func getAppleProfiles(
}
}

v, ok := byName[mdmDecl.Name]
switch {
case !ok:
byName[mdmDecl.Name] = "declaration"
case v == "mobileconfig":
return nil, nil, ctxerr.Wrap(ctx,
fleet.NewInvalidArgumentError(mdmDecl.Name, "A configuration profile with this name already exists."),
"duplicate mobileconfig profile by name")
case v == "declaration":
return nil, nil, ctxerr.Wrap(ctx,
fleet.NewInvalidArgumentError(mdmDecl.Name, "A declaration profile with this name already exists."),
"duplicate declaration profile by name")
default:
// this should never happen but just in case
return nil, nil, ctxerr.Wrap(ctx,
fleet.NewInvalidArgumentError(mdmDecl.Name, "A profile with this name already exists."),
"duplicate profile by name")
}

v, ok = byIdent[mdmDecl.Identifier]
v, ok := byIdent[mdmDecl.Identifier]
switch {
case !ok:
byIdent[mdmDecl.Identifier] = "declaration"
Expand Down

0 comments on commit 841350f

Please sign in to comment.