mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 00:45:19 +00:00
Add cross-platform check for duplicate MDM profile names (#17916)
This commit is contained in:
parent
140dde17b7
commit
841350f556
1
changes/17559-batch-set-duplicate-mdm
Normal file
1
changes/17559-batch-set-duplicate-mdm
Normal file
@ -0,0 +1 @@
|
|||||||
|
- Added cross-platform check for duplicate MDM profiles names in batch set MDM profiles API.
|
@ -1752,6 +1752,34 @@ func (svc *Service) BatchSetMDMAppleProfiles(ctx context.Context, tmID *uint, tm
|
|||||||
profs = append(profs, mdmProf)
|
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 {
|
if dryRun {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -2753,7 +2781,6 @@ func ReconcileAppleDeclarations(
|
|||||||
commander *apple_mdm.MDMAppleCommander,
|
commander *apple_mdm.MDMAppleCommander,
|
||||||
logger kitlog.Logger,
|
logger kitlog.Logger,
|
||||||
) error {
|
) error {
|
||||||
|
|
||||||
// batch set declarations as pending
|
// batch set declarations as pending
|
||||||
changedHosts, err := ds.MDMAppleBatchSetHostDeclarationState(ctx)
|
changedHosts, err := ds.MDMAppleBatchSetHostDeclarationState(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1430,6 +1430,9 @@ func TestMDMBatchSetAppleProfiles(t *testing.T) {
|
|||||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string) error {
|
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string) error {
|
||||||
return nil
|
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 {
|
type testCase struct {
|
||||||
name string
|
name string
|
||||||
@ -1741,6 +1744,9 @@ func TestMDMBatchSetAppleProfilesBoolArgs(t *testing.T) {
|
|||||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, profileUUIDs, uuids []string) error {
|
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, profileUUIDs, uuids []string) error {
|
||||||
return nil
|
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 = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
||||||
ctx = license.NewContext(ctx, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
ctx = license.NewContext(ctx, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
||||||
|
@ -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
|
// getProfilesContents takes file paths and creates a slice of profile payloads
|
||||||
// ready to batch-apply.
|
// ready to batch-apply.
|
||||||
func getProfilesContents(baseDir string, profiles []fleet.MDMProfileSpec) ([]fleet.MDMProfileBatchPayload, error) {
|
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))
|
result := make([]fleet.MDMProfileBatchPayload, 0, len(profiles))
|
||||||
|
|
||||||
for _, profile := range profiles {
|
for _, profile := range profiles {
|
||||||
@ -306,10 +307,10 @@ func getProfilesContents(baseDir string, profiles []fleet.MDMProfileSpec) ([]fle
|
|||||||
}
|
}
|
||||||
name = strings.TrimSpace(mc.Name)
|
name = strings.TrimSpace(mc.Name)
|
||||||
}
|
}
|
||||||
if _, isDuplicate := fileNameMap[name]; isDuplicate {
|
if e, isDuplicate := extByName[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).")
|
return nil, errors.New(fmtDuplicateNameErrMsg(name, e, ext))
|
||||||
}
|
}
|
||||||
fileNameMap[name] = struct{}{}
|
extByName[name] = ext
|
||||||
result = append(result, fleet.MDMProfileBatchPayload{
|
result = append(result, fleet.MDMProfileBatchPayload{
|
||||||
Name: name,
|
Name: name,
|
||||||
Contents: fileContents,
|
Contents: fileContents,
|
||||||
|
@ -88,7 +88,7 @@ func (s *integrationMDMTestSuite) TestAppleDDMBatchUpload() {
|
|||||||
{Name: "bad2", Contents: newDeclBytes(2, `"baz": "bing"`)},
|
{Name: "bad2", Contents: newDeclBytes(2, `"baz": "bing"`)},
|
||||||
}}, http.StatusUnprocessableEntity)
|
}}, http.StatusUnprocessableEntity)
|
||||||
errMsg = extractServerErrorText(res.Body)
|
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
|
// Same identifier should fail
|
||||||
res = s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
|
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.Contains(t, profs["I1"].Detail, "Feature Disabled")
|
||||||
require.Equal(t, &fleet.MDMDeliveryFailed, profs["I2"].Status)
|
require.Equal(t, &fleet.MDMDeliveryFailed, profs["I2"].Status)
|
||||||
require.Contains(t, profs["I2"].Detail, "Feature Disabled")
|
require.Contains(t, profs["I2"].Detail, "Feature Disabled")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *integrationMDMTestSuite) TestDDMNoDeclarationsLeft() {
|
func (s *integrationMDMTestSuite) TestDDMNoDeclarationsLeft() {
|
||||||
|
@ -10990,11 +10990,50 @@ func (s *integrationMDMTestSuite) TestBatchSetMDMProfiles() {
|
|||||||
fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, tm.ID, tm.Name),
|
fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, tm.ID, tm.Name),
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
|
|
||||||
s.lastActivityOfTypeMatches(
|
s.lastActivityOfTypeMatches(
|
||||||
fleet.ActivityTypeEditedDeclarationProfile{}.ActivityName(),
|
fleet.ActivityTypeEditedDeclarationProfile{}.ActivityName(),
|
||||||
fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, tm.ID, tm.Name),
|
fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, tm.ID, tm.Name),
|
||||||
0,
|
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() {
|
func (s *integrationMDMTestSuite) TestBatchSetMDMProfilesBackwardsCompat() {
|
||||||
|
@ -1527,6 +1527,10 @@ func (svc *Service) BatchSetMDMProfiles(
|
|||||||
return ctxerr.Wrap(ctx, err, "validating Windows profiles")
|
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 {
|
if dryRun {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -1578,6 +1582,70 @@ func (svc *Service) BatchSetMDMProfiles(
|
|||||||
return nil
|
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) {
|
func (svc *Service) authorizeBatchProfiles(ctx context.Context, tmID *uint, tmName *string) (*uint, *string, error) {
|
||||||
if tmID != nil && tmName != nil {
|
if tmID != nil && tmName != nil {
|
||||||
svc.authz.SkipAuthorization(ctx) // so that the error message is not replaced by "forbidden"
|
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]
|
v, ok := byIdent[mdmDecl.Identifier]
|
||||||
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]
|
|
||||||
switch {
|
switch {
|
||||||
case !ok:
|
case !ok:
|
||||||
byIdent[mdmDecl.Identifier] = "declaration"
|
byIdent[mdmDecl.Identifier] = "declaration"
|
||||||
|
Loading…
Reference in New Issue
Block a user