@@ -4652,6 +4652,305 @@ func (s *integrationMDMTestSuite) TestBatchSetMDMProfiles() {
}
}
// This tests the new public API endpoint for batch modifying MDM profiles
func ( s * integrationMDMTestSuite ) TestBatchModifyMDMProfiles ( ) {
t := s . T ( )
ctx := context . Background ( )
// create a new team
tm , err := s . ds . NewTeam ( ctx , & fleet . Team { Name : "batch_set_mdm_profiles" } )
require . NoError ( t , err )
bigString := strings . Repeat ( "a" , 1024 * 1024 + 1 )
// Profile is too big
resp := s . Do ( "POST" , "/api/latest/fleet/configuration_profiles/batch" , batchModifyMDMConfigProfilesRequest { ConfigurationProfiles : [ ] fleet . BatchModifyMDMConfigProfilePayload { { Profile : [ ] byte ( bigString ) } } } ,
http . StatusUnprocessableEntity )
require . Contains ( t , extractServerErrorText ( resp . Body ) , "Validation Failed: maximum configuration profile file size is 1 MB" )
// apply an empty set to no-team
s . Do ( "POST" , "/api/latest/fleet/configuration_profiles/batch" , batchModifyMDMConfigProfilesRequest { ConfigurationProfiles : nil } , http . StatusNoContent )
// Nothing changed, so no activity items
s . lastActivityOfTypeDoesNotMatch (
fleet . ActivityTypeEditedMacosProfile { } . ActivityName ( ) ,
` { "team_id": null, "team_name": null} ` ,
0 ,
)
s . lastActivityOfTypeDoesNotMatch (
fleet . ActivityTypeEditedWindowsProfile { } . ActivityName ( ) ,
` { "team_id": null, "team_name": null} ` ,
0 ,
)
s . lastActivityOfTypeDoesNotMatch (
fleet . ActivityTypeEditedDeclarationProfile { } . ActivityName ( ) ,
` { "team_id": null, "team_name": null} ` ,
0 ,
)
// apply to both team id and name
s . Do ( "POST" , "/api/latest/fleet/configuration_profiles/batch" , batchModifyMDMConfigProfilesRequest { ConfigurationProfiles : nil } ,
http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) , "team_name" , tm . Name )
// invalid team name
s . Do ( "POST" , "/api/latest/fleet/configuration_profiles/batch" , batchModifyMDMConfigProfilesRequest { ConfigurationProfiles : nil } ,
http . StatusNotFound , "team_name" , uuid . New ( ) . String ( ) )
// duplicate PayloadDisplayName
s . Do ( "POST" , "/api/latest/fleet/configuration_profiles/batch" , batchModifyMDMConfigProfilesRequest { ConfigurationProfiles : [ ] fleet . BatchModifyMDMConfigProfilePayload {
{ DisplayName : "N1" , Profile : mobileconfigForTest ( "N1" , "I1" ) } ,
{ DisplayName : "N2" , Profile : mobileconfigForTest ( "N1" , "I2" ) } ,
{ DisplayName : "N3" , Profile : syncMLForTest ( "./Foo/Bar" ) } ,
{ DisplayName : "N4" , Profile : declarationForTest ( "D1" ) } ,
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
// profiles with reserved macOS identifiers
for p := range mobileconfig . FleetPayloadIdentifiers ( ) {
res := s . Do ( "POST" , "/api/latest/fleet/configuration_profiles/batch" , batchModifyMDMConfigProfilesRequest { ConfigurationProfiles : [ ] fleet . BatchModifyMDMConfigProfilePayload {
{ DisplayName : "N1" , Profile : mobileconfigForTest ( "N1" , "I1" ) } ,
{ DisplayName : p , Profile : mobileconfigForTest ( p , p ) } ,
{ DisplayName : "N3" , Profile : syncMLForTest ( "./Foo/Bar" ) } ,
{ DisplayName : "N4" , Profile : declarationForTest ( "D1" ) } ,
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
errMsg := extractServerErrorText ( res . Body )
require . Contains ( t , errMsg , fmt . Sprintf ( "Validation Failed: payload identifier %s is not allowed" , p ) )
}
// payloads with reserved types
for p := range mobileconfig . FleetPayloadTypes ( ) {
if p == mobileconfig . FleetCustomSettingsPayloadType {
// FileVault options in the custom settings payload are checked in file_vault_options_test.go
continue
}
res := s . Do ( "POST" , "/api/latest/fleet/configuration_profiles/batch" , batchModifyMDMConfigProfilesRequest { ConfigurationProfiles : [ ] fleet . BatchModifyMDMConfigProfilePayload {
{ DisplayName : "N1" , Profile : mobileconfigForTestWithContent ( "N1" , "I1" , "II1" , p , "" ) } ,
{ DisplayName : "N3" , Profile : syncMLForTest ( "./Foo/Bar" ) } ,
{ DisplayName : "N4" , Profile : declarationForTest ( "D1" ) } ,
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
errMsg := extractServerErrorText ( res . Body )
switch p {
case mobileconfig . FleetFileVaultPayloadType , mobileconfig . FleetRecoveryKeyEscrowPayloadType :
assert . Contains ( t , errMsg , mobileconfig . DiskEncryptionProfileRestrictionErrMsg )
default :
assert . Contains ( t , errMsg , fmt . Sprintf ( "Validation Failed: unsupported PayloadType(s): %s" , p ) )
}
}
// payloads with reserved identifiers
for p := range mobileconfig . FleetPayloadIdentifiers ( ) {
res := s . Do ( "POST" , "/api/latest/fleet/configuration_profiles/batch" , batchModifyMDMConfigProfilesRequest { ConfigurationProfiles : [ ] fleet . BatchModifyMDMConfigProfilePayload {
{ DisplayName : "N1" , Profile : mobileconfigForTestWithContent ( "N1" , "I1" , p , "random" , "" ) } ,
{ DisplayName : "N3" , Profile : syncMLForTest ( "./Foo/Bar" ) } ,
{ DisplayName : "N4" , Profile : declarationForTest ( "D1" ) } ,
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
errMsg := extractServerErrorText ( res . Body )
require . Contains ( t , errMsg , fmt . Sprintf ( "Validation Failed: unsupported PayloadIdentifier(s): %s" , p ) )
}
// profiles with forbidden declaration types
for dt := range fleet . ForbiddenDeclTypes {
res := s . Do ( "POST" , "/api/latest/fleet/configuration_profiles/batch" , batchModifyMDMConfigProfilesRequest { ConfigurationProfiles : [ ] fleet . BatchModifyMDMConfigProfilePayload {
{ DisplayName : "N1" , Profile : mobileconfigForTest ( "N1" , "I1" ) } ,
{ DisplayName : "N3" , Profile : syncMLForTest ( "./Foo/Bar" ) } ,
{ DisplayName : "N4" , Profile : declarationForTestWithType ( "D1" , dt ) } ,
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
errMsg := extractServerErrorText ( res . Body )
require . Contains ( t , errMsg , "Only configuration declarations that don’ t require an asset reference are supported" , dt )
}
// and one more for the software update declaration
res := s . Do ( "POST" , "/api/latest/fleet/configuration_profiles/batch" , batchModifyMDMConfigProfilesRequest { ConfigurationProfiles : [ ] fleet . BatchModifyMDMConfigProfilePayload {
{ DisplayName : "N1" , Profile : mobileconfigForTest ( "N1" , "I1" ) } ,
{ DisplayName : "N3" , Profile : syncMLForTest ( "./Foo/Bar" ) } ,
{ DisplayName : "N4" , Profile : declarationForTestWithType ( "D1" , "com.apple.configuration.softwareupdate.enforcement.specific" ) } ,
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
errMsg := extractServerErrorText ( res . Body )
require . Contains ( t , errMsg , "Declaration profile can’ t include OS updates settings. To control these settings, go to OS updates." )
// invalid JSON
res = s . Do ( "POST" , "/api/latest/fleet/configuration_profiles/batch" , batchModifyMDMConfigProfilesRequest { ConfigurationProfiles : [ ] fleet . BatchModifyMDMConfigProfilePayload {
{ DisplayName : "N1" , Profile : mobileconfigForTest ( "N1" , "I1" ) } ,
{ DisplayName : "N3" , Profile : syncMLForTest ( "./Foo/Bar" ) } ,
{ DisplayName : "N4" , Profile : [ ] byte ( ` { "foo":} ` ) } ,
} } , http . StatusBadRequest , "team_id" , fmt . Sprint ( tm . ID ) )
errMsg = extractServerErrorText ( res . Body )
require . Contains ( t , errMsg , "The file should include valid JSON" )
// profiles with reserved Windows location URIs
// bitlocker
res = s . Do ( "POST" , "/api/latest/fleet/configuration_profiles/batch" , batchModifyMDMConfigProfilesRequest { ConfigurationProfiles : [ ] fleet . BatchModifyMDMConfigProfilePayload {
{ DisplayName : "N1" , Profile : mobileconfigForTest ( "N1" , "I1" ) } ,
{ DisplayName : syncml . FleetBitLockerTargetLocURI , Profile : syncMLForTest ( fmt . Sprintf ( "%s/Foo" , syncml . FleetBitLockerTargetLocURI ) ) } ,
{ DisplayName : "N3" , Profile : syncMLForTest ( "./Foo/Bar" ) } ,
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
errMsg = extractServerErrorText ( res . Body )
assert . Contains ( t , errMsg , syncml . DiskEncryptionProfileRestrictionErrMsg )
// os updates
res = s . Do ( "POST" , "/api/latest/fleet/configuration_profiles/batch" , batchModifyMDMConfigProfilesRequest { ConfigurationProfiles : [ ] fleet . BatchModifyMDMConfigProfilePayload {
{ DisplayName : "N1" , Profile : mobileconfigForTest ( "N1" , "I1" ) } ,
{ DisplayName : syncml . FleetOSUpdateTargetLocURI , Profile : syncMLForTest ( fmt . Sprintf ( "%s/Foo" , syncml . FleetOSUpdateTargetLocURI ) ) } ,
{ DisplayName : "N3" , Profile : syncMLForTest ( "./Foo/Bar" ) } ,
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
errMsg = extractServerErrorText ( res . Body )
require . Contains ( t , errMsg , "Custom configuration profiles can't include Windows updates settings. To control these settings, use the mdm.windows_updates option." )
// invalid windows tag
res = s . Do ( "POST" , "/api/latest/fleet/configuration_profiles/batch" , batchModifyMDMConfigProfilesRequest { ConfigurationProfiles : [ ] fleet . BatchModifyMDMConfigProfilePayload {
{ DisplayName : "N3" , Profile : [ ] byte ( ` <Exec></Exec> ` ) } ,
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
errMsg = extractServerErrorText ( res . Body )
require . Contains ( t , errMsg , "Windows configuration profiles can only have <Replace> or <Add> top level elements." )
// invalid xml
res = s . Do ( "POST" , "/api/latest/fleet/configuration_profiles/batch" , batchModifyMDMConfigProfilesRequest { ConfigurationProfiles : [ ] fleet . BatchModifyMDMConfigProfilePayload {
{ DisplayName : "N3" , Profile : [ ] byte ( ` foo ` ) } ,
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
errMsg = extractServerErrorText ( res . Body )
require . Contains ( t , errMsg , "Windows configuration profiles can only have <Replace> or <Add> top level elements." )
// successfully apply windows and macOS a profiles for the team, but it's a dry run
s . Do ( "POST" , "/api/latest/fleet/configuration_profiles/batch" , batchModifyMDMConfigProfilesRequest { ConfigurationProfiles : [ ] fleet . BatchModifyMDMConfigProfilePayload {
{ DisplayName : "N1" , Profile : mobileconfigForTest ( "N1" , "I1" ) } ,
{ DisplayName : "N2" , Profile : syncMLForTest ( "./Foo/Bar" ) } ,
{ DisplayName : "N4" , Profile : declarationForTest ( "D1" ) } ,
} } , http . StatusNoContent , "team_id" , fmt . Sprint ( tm . ID ) , "dry_run" , "true" )
s . assertConfigProfilesByIdentifier ( & tm . ID , "I1" , false )
s . assertWindowsConfigProfilesByName ( & tm . ID , "N1" , false )
// successfully apply for a team and verify activities
s . Do ( "POST" , "/api/latest/fleet/configuration_profiles/batch" , batchModifyMDMConfigProfilesRequest { ConfigurationProfiles : [ ] fleet . BatchModifyMDMConfigProfilePayload {
{ DisplayName : "N1" , Profile : mobileconfigForTest ( "N1" , "I1" ) } ,
{ DisplayName : "N2" , Profile : syncMLForTest ( "./Foo/Bar" ) } ,
{ DisplayName : "N4" , Profile : declarationForTest ( "D1" ) } ,
} } , http . StatusNoContent , "team_id" , fmt . Sprint ( tm . ID ) )
s . assertConfigProfilesByIdentifier ( & tm . ID , "I1" , true )
s . assertWindowsConfigProfilesByName ( & tm . ID , "N2" , true )
s . lastActivityOfTypeMatches (
fleet . ActivityTypeEditedMacosProfile { } . ActivityName ( ) ,
fmt . Sprintf ( ` { "team_id": %d, "team_name": %q} ` , tm . ID , tm . Name ) ,
0 ,
)
s . lastActivityOfTypeMatches (
fleet . ActivityTypeEditedWindowsProfile { } . ActivityName ( ) ,
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 ,
)
// batch-apply profiles with labels
lbl1 , err := s . ds . NewLabel ( ctx , & fleet . Label { Name : "L1" , Query : "select 1;" } )
require . NoError ( t , err )
lbl2 , err := s . ds . NewLabel ( ctx , & fleet . Label { Name : "L2" , Query : "select 1;" } )
require . NoError ( t , err )
lbl3 , err := s . ds . NewLabel ( ctx , & fleet . Label { Name : "L3" , Query : "select 1;" } )
require . NoError ( t , err )
// attempt with an invalid label name
res = s . Do ( "POST" , "/api/latest/fleet/configuration_profiles/batch" , batchModifyMDMConfigProfilesRequest { ConfigurationProfiles : [ ] fleet . BatchModifyMDMConfigProfilePayload {
{ DisplayName : "N1" , Profile : mobileconfigForTest ( "N1" , "I1" ) , LabelsIncludeAll : [ ] string { lbl1 . Name , "no-such-label" } } ,
} } , http . StatusBadRequest )
msg := extractServerErrorText ( res . Body )
require . Contains ( t , msg , "some or all the labels provided don't exist" )
// mix of labels fields
res = s . Do ( "POST" , "/api/latest/fleet/configuration_profiles/batch" , batchModifyMDMConfigProfilesRequest { ConfigurationProfiles : [ ] fleet . BatchModifyMDMConfigProfilePayload {
{ DisplayName : "N1" , Profile : mobileconfigForTest ( "N1" , "I1" ) , LabelsIncludeAll : [ ] string { lbl1 . Name } , LabelsExcludeAny : [ ] string { lbl2 . Name } } ,
} } , http . StatusUnprocessableEntity )
msg = extractServerErrorText ( res . Body )
require . Contains ( t , msg , ` For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included. ` )
// successful batch-set
s . Do ( "POST" , "/api/latest/fleet/configuration_profiles/batch" , batchModifyMDMConfigProfilesRequest { ConfigurationProfiles : [ ] fleet . BatchModifyMDMConfigProfilePayload {
{ DisplayName : "N1" , Profile : mobileconfigForTest ( "N1" , "I1" ) , LabelsIncludeAny : [ ] string { lbl1 . Name , lbl2 . Name } } ,
{ DisplayName : "N2" , Profile : syncMLForTest ( "./Foo/Bar" ) , LabelsIncludeAll : [ ] string { lbl1 . Name } } ,
{ DisplayName : "N4" , Profile : declarationForTest ( "D1" ) , LabelsExcludeAny : [ ] string { lbl2 . Name } } ,
} } , http . StatusNoContent )
// confirm expected results
var listResp listMDMConfigProfilesResponse
s . DoJSON ( "GET" , "/api/latest/fleet/configuration_profiles" , nil , http . StatusOK , & listResp )
require . Len ( t , listResp . Profiles , 3 )
require . Equal ( t , "N1" , listResp . Profiles [ 0 ] . Name )
require . Equal ( t , "N2" , listResp . Profiles [ 1 ] . Name )
require . Equal ( t , "N4" , listResp . Profiles [ 2 ] . Name )
require . Equal ( t , listResp . Profiles [ 0 ] . LabelsIncludeAny , [ ] fleet . ConfigurationProfileLabel {
{ LabelID : lbl1 . ID , LabelName : lbl1 . Name } ,
{ LabelID : lbl2 . ID , LabelName : lbl2 . Name } ,
} )
require . Nil ( t , listResp . Profiles [ 0 ] . LabelsExcludeAny )
require . Equal ( t , listResp . Profiles [ 1 ] . LabelsIncludeAll , [ ] fleet . ConfigurationProfileLabel {
{ LabelID : lbl1 . ID , LabelName : lbl1 . Name } ,
} )
require . Nil ( t , listResp . Profiles [ 1 ] . LabelsExcludeAny )
require . Equal ( t , listResp . Profiles [ 2 ] . LabelsExcludeAny , [ ] fleet . ConfigurationProfileLabel {
{ LabelID : lbl2 . ID , LabelName : lbl2 . Name } ,
} )
require . Nil ( t , listResp . Profiles [ 2 ] . LabelsIncludeAll )
// successful batch-set that updates some labels
s . Do ( "POST" , "/api/latest/fleet/configuration_profiles/batch" , batchModifyMDMConfigProfilesRequest { ConfigurationProfiles : [ ] fleet . BatchModifyMDMConfigProfilePayload {
{ DisplayName : "N1" , Profile : mobileconfigForTest ( "N1" , "I1" ) , LabelsExcludeAny : [ ] string { lbl1 . Name , lbl3 . Name } } ,
{ DisplayName : "N2" , Profile : syncMLForTest ( "./Foo/Bar" ) , LabelsIncludeAll : [ ] string { lbl2 . Name } } ,
} } , http . StatusNoContent )
listResp = listMDMConfigProfilesResponse { }
s . DoJSON ( "GET" , "/api/latest/fleet/configuration_profiles" , nil , http . StatusOK , & listResp )
require . Len ( t , listResp . Profiles , 2 )
require . Equal ( t , "N1" , listResp . Profiles [ 0 ] . Name )
require . Equal ( t , "N2" , listResp . Profiles [ 1 ] . Name )
require . Equal ( t , listResp . Profiles [ 0 ] . LabelsExcludeAny , [ ] fleet . ConfigurationProfileLabel {
{ LabelID : lbl1 . ID , LabelName : lbl1 . Name } ,
{ LabelID : lbl3 . ID , LabelName : lbl3 . Name } ,
} )
require . Nil ( t , listResp . Profiles [ 0 ] . LabelsIncludeAll )
require . Equal ( t , listResp . Profiles [ 1 ] . LabelsIncludeAll , [ ] fleet . ConfigurationProfileLabel {
{ LabelID : lbl2 . ID , LabelName : lbl2 . Name } ,
} )
require . Nil ( t , listResp . Profiles [ 1 ] . LabelsExcludeAny )
// 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 . BatchModifyMDMConfigProfilePayload
expectErr string
} {
{
payload : [ ] fleet . BatchModifyMDMConfigProfilePayload { { DisplayName : "N1" , Profile : mcBytes } , { DisplayName : "N1" , Profile : winBytes } } ,
expectErr : "More than one configuration profile have the same name 'N1'" ,
} ,
{
payload : [ ] fleet . BatchModifyMDMConfigProfilePayload { { DisplayName : "N1" , Profile : declBytes } , { DisplayName : "N1" , Profile : winBytes } } ,
expectErr : "More than one configuration profile have the same name 'N1'" ,
} ,
{
payload : [ ] fleet . BatchModifyMDMConfigProfilePayload { { DisplayName : "N1" , Profile : mcBytes } , { DisplayName : "N1" , Profile : declBytes } } ,
expectErr : "More than one configuration profile have the same name 'N1'" ,
} ,
} {
// team profiles
res = s . Do ( "POST" , "/api/latest/fleet/configuration_profiles/batch" , batchModifyMDMConfigProfilesRequest { ConfigurationProfiles : p . payload } ,
http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
errMsg = extractServerErrorText ( res . Body )
require . Contains ( t , errMsg , p . expectErr )
// no team profiles
res = s . Do ( "POST" , "/api/latest/fleet/configuration_profiles/batch" , batchModifyMDMConfigProfilesRequest { ConfigurationProfiles : p . payload } , http . StatusUnprocessableEntity )
errMsg = extractServerErrorText ( res . Body )
require . Contains ( t , errMsg , p . expectErr )
}
}
func ( s * integrationMDMTestSuite ) TestBatchSetMDMProfilesBackwardsCompat ( ) {
t := s . T ( )
ctx := context . Background ( )