Add cross-platform check for duplicate MDM profile names (#17916)

This commit is contained in:
Sarah Gillespie 2024-03-29 14:55:03 -05:00 committed by GitHub
parent 140dde17b7
commit 841350f556
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 149 additions and 27 deletions

View File

@ -0,0 +1 @@
- Added cross-platform check for duplicate MDM profiles names in batch set MDM profiles API.

View File

@ -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(
"Couldnt 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(
"Couldnt 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
}
@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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