2024-04-18 21:01:37 +00:00
package service
import (
"bytes"
"context"
"crypto/md5" // nolint:gosec // used only for tests
"crypto/x509"
2025-12-10 23:04:17 +00:00
"database/sql"
2025-11-12 19:59:09 +00:00
_ "embed"
2024-04-18 21:01:37 +00:00
"encoding/json"
"encoding/xml"
"fmt"
"io"
"net/http"
2024-08-30 13:04:10 +00:00
"net/url"
2024-06-25 19:26:28 +00:00
"sort"
2024-04-18 21:01:37 +00:00
"strings"
"testing"
"time"
2025-08-18 16:31:53 +00:00
shared_mdm "github.com/fleetdm/fleet/v4/pkg/mdm"
2024-04-18 21:01:37 +00:00
"github.com/fleetdm/fleet/v4/pkg/mdm/mdmtest"
"github.com/fleetdm/fleet/v4/pkg/optjson"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/fleet"
servermdm "github.com/fleetdm/fleet/v4/server/mdm"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
2024-09-10 22:44:58 +00:00
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/godep"
2025-01-06 19:16:34 +00:00
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
2025-11-12 19:59:09 +00:00
scepserver "github.com/fleetdm/fleet/v4/server/mdm/scep/server"
2024-04-18 21:01:37 +00:00
"github.com/fleetdm/fleet/v4/server/ptr"
2025-07-16 18:08:27 +00:00
"github.com/fleetdm/fleet/v4/server/service/contract"
2025-11-12 19:59:09 +00:00
"github.com/fleetdm/fleet/v4/server/service/integrationtest/scep_server"
2024-04-18 21:01:37 +00:00
"github.com/fleetdm/fleet/v4/server/test"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
2025-11-21 14:13:36 +00:00
micromdm "github.com/micromdm/micromdm/mdm/mdm"
2025-03-19 13:27:55 +00:00
"github.com/micromdm/plist"
2024-09-10 19:52:17 +00:00
"github.com/smallstep/pkcs7"
2024-04-18 21:01:37 +00:00
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func ( s * integrationMDMTestSuite ) signedProfilesMatch ( want , got [ ] [ ] byte ) {
t := s . T ( )
rootCA := x509 . NewCertPool ( )
2024-05-30 21:18:42 +00:00
assets , err := s . ds . GetAllMDMConfigAssetsByName ( context . Background ( ) , [ ] fleet . MDMAssetName {
fleet . MDMAssetCACert ,
2024-10-09 18:47:27 +00:00
} , nil )
2024-05-30 21:18:42 +00:00
require . NoError ( t , err )
require . True ( t , rootCA . AppendCertsFromPEM ( assets [ fleet . MDMAssetCACert ] . Value ) )
2024-04-18 21:01:37 +00:00
// verify that all the profiles were signed usign the SCEP certificate,
// and grab their contents
signedContents := [ ] [ ] byte { }
for _ , prof := range got {
p7 , err := pkcs7 . Parse ( prof )
require . NoError ( t , err )
require . NoError ( t , p7 . VerifyWithChain ( rootCA ) )
signedContents = append ( signedContents , p7 . Content )
}
// verify that contents match
require . ElementsMatch ( t , want , signedContents )
}
func ( s * integrationMDMTestSuite ) TestAppleProfileManagement ( ) {
t := s . T ( )
ctx := context . Background ( )
err := s . ds . ApplyEnrollSecrets ( ctx , nil , [ ] * fleet . EnrollSecret { { Secret : t . Name ( ) } } )
require . NoError ( t , err )
globalProfiles := [ ] [ ] byte {
mobileconfigForTest ( "N1" , "I1" ) ,
mobileconfigForTest ( "N2" , "I2" ) ,
}
2024-10-18 17:38:26 +00:00
wantGlobalProfiles := globalProfiles
wantGlobalProfiles = append (
wantGlobalProfiles ,
2024-04-18 21:01:37 +00:00
setupExpectedFleetdProfile ( t , s . server . URL , t . Name ( ) , nil ) ,
)
// add global profiles
s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" , batchSetMDMAppleProfilesRequest { Profiles : globalProfiles } , http . StatusNoContent )
2024-12-17 22:14:12 +00:00
// invalid secrets
2025-01-30 11:17:36 +00:00
invalidSecretsProfile := [ ] byte ( `
2024-12-17 22:14:12 +00:00
< ? xml version = "1.0" encoding = "UTF-8" ? >
< ! DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd" >
< plist version = "1.0" >
< dict >
< key > PayloadContent < / key >
< array / >
< key > PayloadDisplayName < / key >
2025-08-22 14:37:12 +00:00
< string > My profile < / string >
2024-12-17 22:14:12 +00:00
< key > PayloadIdentifier < / key >
2025-08-22 14:37:12 +00:00
< string > $ FLEET_SECRET_INVALID < / string >
2024-12-17 22:14:12 +00:00
< key > PayloadType < / key >
< string > Configuration < / string >
< key > PayloadUUID < / key >
< string > 601E0 B42 - 0 989 - 4 FAD - A61B - 18656 BA3670E < / string >
< key > PayloadVersion < / key >
< integer > 1 < / integer >
< / dict >
< / plist >
` )
res := s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" , batchSetMDMAppleProfilesRequest { Profiles : [ ] [ ] byte { invalidSecretsProfile } } , http . StatusUnprocessableEntity )
errMsg := extractServerErrorText ( res . Body )
require . Contains ( t , errMsg , "$FLEET_SECRET_INVALID" )
2025-08-22 14:37:12 +00:00
invalidSecretsProfile = [ ] byte ( `
< ? xml version = "1.0" encoding = "UTF-8" ? >
< ! DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd" >
< plist version = "1.0" >
< dict >
< key > PayloadContent < / key >
< array / >
< key > PayloadDisplayName < / key >
< string > $ FLEET_SECRET_INVALID < / string >
< key > PayloadIdentifier < / key >
< string > N3 < / string >
< key > PayloadType < / key >
< string > Configuration < / string >
< key > PayloadUUID < / key >
< string > 601E0 B42 - 0 989 - 4 FAD - A61B - 18656 BA3670E < / string >
< key > PayloadVersion < / key >
< integer > 1 < / integer >
< / dict >
< / plist >
` )
res = s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" , batchSetMDMAppleProfilesRequest { Profiles : [ ] [ ] byte { invalidSecretsProfile } } , http . StatusUnprocessableEntity )
errMsg = extractServerErrorText ( res . Body )
require . Contains ( t , errMsg , "PayloadDisplayName cannot contain FLEET_SECRET variables" )
2024-04-18 21:01:37 +00:00
// create a new team
tm , err := s . ds . NewTeam ( ctx , & fleet . Team { Name : "batch_set_mdm_profiles" } )
require . NoError ( t , err )
// add an enroll secret so the fleetd profiles differ
var teamResp teamEnrollSecretsResponse
s . DoJSON ( "PATCH" , fmt . Sprintf ( "/api/latest/fleet/teams/%d/secrets" , tm . ID ) ,
modifyTeamEnrollSecretsRequest {
Secrets : [ ] fleet . EnrollSecret { { Secret : "team1_enroll_sec" } } ,
} , http . StatusOK , & teamResp )
teamProfiles := [ ] [ ] byte {
mobileconfigForTest ( "N3" , "I3" ) ,
}
2024-10-18 17:38:26 +00:00
wantTeamProfiles := teamProfiles
wantTeamProfiles = append (
wantTeamProfiles ,
2024-04-18 21:01:37 +00:00
setupExpectedFleetdProfile ( t , s . server . URL , "team1_enroll_sec" , & tm . ID ) ,
)
// add profiles to the team
2024-10-18 17:38:26 +00:00
s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" , batchSetMDMAppleProfilesRequest { Profiles : teamProfiles } , http . StatusNoContent ,
"team_id" , fmt . Sprint ( tm . ID ) )
2024-04-18 21:01:37 +00:00
// create a non-macOS host
_ , err = s . ds . NewHost ( context . Background ( ) , & fleet . Host {
ID : 1 ,
OsqueryHostID : ptr . String ( "non-macos-host" ) ,
NodeKey : ptr . String ( "non-macos-host" ) ,
UUID : uuid . New ( ) . String ( ) ,
Hostname : fmt . Sprintf ( "%sfoo.local.non.macos" , t . Name ( ) ) ,
Platform : "windows" ,
} )
require . NoError ( t , err )
// create a host that's not enrolled into MDM
_ , err = s . ds . NewHost ( context . Background ( ) , & fleet . Host {
ID : 2 ,
OsqueryHostID : ptr . String ( "not-mdm-enrolled" ) ,
NodeKey : ptr . String ( "not-mdm-enrolled" ) ,
UUID : uuid . New ( ) . String ( ) ,
Hostname : fmt . Sprintf ( "%sfoo.local.not.enrolled" , t . Name ( ) ) ,
Platform : "darwin" ,
} )
require . NoError ( t , err )
// Create a host and then enroll to MDM.
host , mdmDevice := createHostThenEnrollMDM ( s . ds , s . server . URL , t )
setupPusher ( s , t , mdmDevice )
// trigger a profile sync
s . awaitTriggerProfileSchedule ( t )
installs , removes := checkNextPayloads ( t , mdmDevice , false )
// verify that we received all profiles
s . signedProfilesMatch (
2024-05-30 21:18:42 +00:00
append ( wantGlobalProfiles , setupExpectedCAProfile ( t , s . ds ) ) ,
2024-04-18 21:01:37 +00:00
installs ,
)
require . Empty ( t , removes )
expectedNoTeamSummary := fleet . MDMProfilesSummary {
Pending : 0 ,
Failed : 0 ,
Verifying : 1 ,
Verified : 0 ,
}
expectedTeamSummary := fleet . MDMProfilesSummary { }
s . checkMDMProfilesSummaries ( t , nil , expectedNoTeamSummary , & expectedNoTeamSummary )
s . checkMDMProfilesSummaries ( t , & tm . ID , expectedTeamSummary , & expectedTeamSummary ) // empty because no hosts in team
// add the host to a team
2025-07-17 14:20:49 +00:00
err = s . ds . AddHostsToTeam ( ctx , fleet . NewAddHostsToTeamParams ( & tm . ID , [ ] uint { host . ID } ) )
2024-04-18 21:01:37 +00:00
require . NoError ( t , err )
// trigger a profile sync
s . awaitTriggerProfileSchedule ( t )
installs , removes = checkNextPayloads ( t , mdmDevice , false )
// verify that we should install the team profile
s . signedProfilesMatch ( wantTeamProfiles , installs )
// verify that we should delete both profiles
require . ElementsMatch ( t , [ ] string { "I1" , "I2" } , removes )
expectedNoTeamSummary = fleet . MDMProfilesSummary { }
expectedTeamSummary = fleet . MDMProfilesSummary {
Pending : 0 ,
Failed : 0 ,
Verifying : 1 ,
Verified : 0 ,
}
s . checkMDMProfilesSummaries ( t , nil , expectedNoTeamSummary , & expectedNoTeamSummary ) // empty because host was transferred
s . checkMDMProfilesSummaries ( t , & tm . ID , expectedTeamSummary , & expectedTeamSummary ) // host now verifying team profiles
2024-12-20 21:40:23 +00:00
// Use secret variables in a profile
secretIdentifier := "secret-identifier-1"
secretType := "secret.type.1"
secretProfile := string ( mobileconfigForTest ( "NS1" , "IS1" ) )
2025-08-14 22:33:47 +00:00
req := createSecretVariablesRequest {
2024-12-20 21:40:23 +00:00
SecretVariables : [ ] fleet . SecretVariable {
{
Name : "FLEET_SECRET_IDENTIFIER" ,
Value : secretIdentifier ,
} ,
{
Name : "FLEET_SECRET_TYPE" ,
Value : secretType ,
} ,
{
Name : "FLEET_SECRET_PROFILE" ,
Value : secretProfile ,
} ,
} ,
}
2025-08-14 22:33:47 +00:00
secretResp := createSecretVariablesResponse { }
2024-12-20 21:40:23 +00:00
s . DoJSON ( "PUT" , "/api/latest/fleet/spec/secret_variables" , req , http . StatusOK , & secretResp )
2024-04-18 21:01:37 +00:00
// set new team profiles (delete + addition)
teamProfiles = [ ] [ ] byte {
mobileconfigForTest ( "N4" , "I4" ) ,
2024-12-20 21:40:23 +00:00
mobileconfigForTestWithContent ( "N5" , "I5" , "$FLEET_SECRET_IDENTIFIER" , "${FLEET_SECRET_TYPE}" ,
2025-08-22 14:37:12 +00:00
"InnerName5" ) ,
2024-12-20 21:40:23 +00:00
// The whole profile is one big secret.
[ ] byte ( "$FLEET_SECRET_PROFILE" ) ,
}
// We deep copy one of the team profiles because we will modify the slice in place, and we want to keep the originals for later.
wantTeamProfiles = [ ] [ ] byte {
teamProfiles [ 0 ] ,
make ( [ ] byte , len ( teamProfiles [ 1 ] ) ) ,
{ } ,
2024-04-18 21:01:37 +00:00
}
2024-12-20 21:40:23 +00:00
copy ( wantTeamProfiles [ 1 ] , teamProfiles [ 1 ] )
2024-10-18 17:38:26 +00:00
s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" , batchSetMDMAppleProfilesRequest { Profiles : teamProfiles } , http . StatusNoContent ,
"team_id" , fmt . Sprint ( tm . ID ) )
2024-04-18 21:01:37 +00:00
// trigger a profile sync
s . awaitTriggerProfileSchedule ( t )
installs , removes = checkNextPayloads ( t , mdmDevice , false )
2024-12-20 21:40:23 +00:00
// Manually replace the expected secret variables in the profile
wantTeamProfiles [ 1 ] = [ ] byte ( strings . ReplaceAll ( string ( wantTeamProfiles [ 1 ] ) , "$FLEET_SECRET_IDENTIFIER" , secretIdentifier ) )
wantTeamProfiles [ 1 ] = [ ] byte ( strings . ReplaceAll ( string ( wantTeamProfiles [ 1 ] ) , "${FLEET_SECRET_TYPE}" , secretType ) )
wantTeamProfiles [ 2 ] = [ ] byte ( secretProfile )
2024-04-18 21:01:37 +00:00
// verify that we should install the team profiles
s . signedProfilesMatch ( wantTeamProfiles , installs )
// verify that we should delete the old team profiles
require . ElementsMatch ( t , [ ] string { "I3" } , removes )
s . checkMDMProfilesSummaries ( t , nil , expectedNoTeamSummary , & expectedNoTeamSummary ) // empty because host was transferred
s . checkMDMProfilesSummaries ( t , & tm . ID , expectedTeamSummary , & expectedTeamSummary ) // host still verifying team profiles
2024-12-30 23:58:39 +00:00
// Upload the same profiles again. No changes expected.
s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" , batchSetMDMAppleProfilesRequest { Profiles : teamProfiles } , http . StatusNoContent ,
"team_id" , fmt . Sprint ( tm . ID ) )
2024-04-18 21:01:37 +00:00
s . awaitTriggerProfileSchedule ( t )
installs , removes = checkNextPayloads ( t , mdmDevice , false )
require . Empty ( t , installs )
require . Empty ( t , removes )
2024-12-30 23:58:39 +00:00
// Change the secret variable and upload the profiles again. We should see the profile with updated secret installed.
2025-08-22 14:37:12 +00:00
secretType = "new.secret.type.1"
2025-08-14 22:33:47 +00:00
req = createSecretVariablesRequest {
2024-12-30 23:58:39 +00:00
SecretVariables : [ ] fleet . SecretVariable {
{
2025-08-22 14:37:12 +00:00
Name : "FLEET_SECRET_IDENTIFIER" ,
Value : secretIdentifier , // did not change
} ,
{
Name : "FLEET_SECRET_TYPE" ,
Value : secretType , // changed
2024-12-30 23:58:39 +00:00
} ,
{
Name : "FLEET_SECRET_PROFILE" ,
Value : secretProfile , // did not change
} ,
} ,
}
s . DoJSON ( "PUT" , "/api/latest/fleet/spec/secret_variables" , req , http . StatusOK , & secretResp )
s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" , batchSetMDMAppleProfilesRequest { Profiles : teamProfiles } , http . StatusNoContent ,
"team_id" , fmt . Sprint ( tm . ID ) )
s . awaitTriggerProfileSchedule ( t )
installs , removes = checkNextPayloads ( t , mdmDevice , false )
// Manually replace the expected secret variables in the profile
wantTeamProfilesChanged := [ ] [ ] byte {
teamProfiles [ 1 ] ,
}
wantTeamProfilesChanged [ 0 ] = [ ] byte ( strings . ReplaceAll ( string ( wantTeamProfilesChanged [ 0 ] ) , "$FLEET_SECRET_IDENTIFIER" ,
secretIdentifier ) )
wantTeamProfilesChanged [ 0 ] = [ ] byte ( strings . ReplaceAll ( string ( wantTeamProfilesChanged [ 0 ] ) , "${FLEET_SECRET_TYPE}" , secretType ) )
// verify that we should install the team profiles
s . signedProfilesMatch ( wantTeamProfilesChanged , installs )
wantTeamProfiles [ 1 ] = wantTeamProfilesChanged [ 0 ]
// No profiles should be deleted
assert . Empty ( t , removes )
2024-12-20 21:40:23 +00:00
// Clear the profiles using the new (non-deprecated) endpoint.
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : nil } , http . StatusNoContent , "team_id" ,
fmt . Sprint ( tm . ID ) , "dry_run" , "true" )
s . assertConfigProfilesByIdentifier ( & tm . ID , "IS1" , true )
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : nil } , http . StatusNoContent , "team_id" ,
fmt . Sprint ( tm . ID ) , "dry_run" , "false" )
s . assertConfigProfilesByIdentifier ( & tm . ID , "IS1" , false )
s . awaitTriggerProfileSchedule ( t )
installs , removes = checkNextPayloads ( t , mdmDevice , false )
require . Empty ( t , installs )
assert . Len ( t , removes , 3 )
// And reapply the same profiles using the new (non-deprecated) endpoint.
batchRequest := batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : "N4" , Contents : teamProfiles [ 0 ] } ,
{ Name : "N5" , Contents : teamProfiles [ 1 ] } ,
{ Name : "NS1" , Contents : teamProfiles [ 2 ] } ,
} }
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchRequest , http . StatusNoContent , "team_id" , fmt . Sprint ( tm . ID ) , "dry_run" , "true" )
s . assertConfigProfilesByIdentifier ( & tm . ID , "I4" , false )
s . assertConfigProfilesByIdentifier ( & tm . ID , "I5" , false )
s . assertConfigProfilesByIdentifier ( & tm . ID , "IS1" , false )
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchRequest , http . StatusNoContent , "team_id" , fmt . Sprint ( tm . ID ) )
s . assertConfigProfilesByIdentifier ( & tm . ID , "IS1" , true )
s . awaitTriggerProfileSchedule ( t )
installs , removes = checkNextPayloads ( t , mdmDevice , false )
assert . Empty ( t , removes )
// verify that we should install the team profiles
s . signedProfilesMatch ( wantTeamProfiles , installs )
2024-12-30 23:58:39 +00:00
// Upload the same profiles again. No changes expected.
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchRequest , http . StatusNoContent , "team_id" , fmt . Sprint ( tm . ID ) , "dry_run" , "true" )
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchRequest , http . StatusNoContent , "team_id" , fmt . Sprint ( tm . ID ) )
s . awaitTriggerProfileSchedule ( t )
installs , removes = checkNextPayloads ( t , mdmDevice , false )
require . Empty ( t , installs )
require . Empty ( t , removes )
// Change the secret variable and upload the profiles again. We should see the profile with updated secret installed.
2025-08-22 14:37:12 +00:00
secretType = "new2.secret.type.1"
2025-08-14 22:33:47 +00:00
req = createSecretVariablesRequest {
2024-12-30 23:58:39 +00:00
SecretVariables : [ ] fleet . SecretVariable {
{
2025-08-22 14:37:12 +00:00
Name : "FLEET_SECRET_IDENTIFIER" ,
Value : secretIdentifier , // did not change
} ,
{
Name : "FLEET_SECRET_TYPE" ,
Value : secretType , // changed
2024-12-30 23:58:39 +00:00
} ,
{
Name : "FLEET_SECRET_PROFILE" ,
Value : secretProfile , // did not change
} ,
} ,
}
s . DoJSON ( "PUT" , "/api/latest/fleet/spec/secret_variables" , req , http . StatusOK , & secretResp )
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchRequest , http . StatusNoContent , "team_id" , fmt . Sprint ( tm . ID ) , "dry_run" , "true" )
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchRequest , http . StatusNoContent , "team_id" , fmt . Sprint ( tm . ID ) )
s . awaitTriggerProfileSchedule ( t )
installs , removes = checkNextPayloads ( t , mdmDevice , false )
// Manually replace the expected secret variables in the profile
wantTeamProfilesChanged = [ ] [ ] byte {
teamProfiles [ 1 ] ,
}
wantTeamProfilesChanged [ 0 ] = [ ] byte ( strings . ReplaceAll ( string ( wantTeamProfilesChanged [ 0 ] ) , "$FLEET_SECRET_IDENTIFIER" ,
secretIdentifier ) )
wantTeamProfilesChanged [ 0 ] = [ ] byte ( strings . ReplaceAll ( string ( wantTeamProfilesChanged [ 0 ] ) , "${FLEET_SECRET_TYPE}" , secretType ) )
// verify that we should install the team profiles
s . signedProfilesMatch ( wantTeamProfilesChanged , installs )
wantTeamProfiles [ 1 ] = wantTeamProfilesChanged [ 0 ]
// No profiles should be deleted
assert . Empty ( t , removes )
2024-04-18 21:01:37 +00:00
var hostResp getHostResponse
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/v1/fleet/hosts/%d" , host . ID ) , getHostRequest { } , http . StatusOK , & hostResp )
require . NotEmpty ( t , hostResp . Host . MDM . Profiles )
resProfiles := * hostResp . Host . MDM . Profiles
// two extra profiles: fleetd config and root CA
require . Len ( t , resProfiles , len ( wantTeamProfiles ) + 2 )
s . checkMDMProfilesSummaries ( t , nil , expectedNoTeamSummary , & expectedNoTeamSummary ) // empty because host was transferred
s . checkMDMProfilesSummaries ( t , & tm . ID , expectedTeamSummary , & expectedTeamSummary ) // host still verifying team profiles
// add a new profile to the team
mcUUID := "a" + uuid . NewString ( )
2025-08-03 06:18:13 +00:00
prof := mcBytesForTest ( "name-" + mcUUID , "identifier-" + mcUUID , mcUUID )
2024-04-18 21:01:37 +00:00
wantTeamProfiles = append ( wantTeamProfiles , prof )
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
stmt := ` INSERT INTO mdm_apple_configuration_profiles (profile_uuid, team_id, name, identifier, mobileconfig, checksum, uploaded_at) VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP); `
2025-08-03 06:18:13 +00:00
_ , err := q . ExecContext ( context . Background ( ) , stmt , mcUUID , tm . ID , "name-" + mcUUID , "identifier-" + mcUUID , prof , test . MakeTestBytes ( ) )
2024-04-18 21:01:37 +00:00
return err
} )
s . awaitTriggerProfileSchedule ( t )
installs , removes = checkNextPayloads ( t , mdmDevice , false )
require . Len ( t , installs , 1 )
s . signedProfilesMatch ( [ ] [ ] byte { prof } , installs )
require . Empty ( t , removes )
s . checkMDMProfilesSummaries ( t , & tm . ID , fleet . MDMProfilesSummary { Verifying : 1 } , nil )
// can't resend profile while verifying
2024-12-17 22:14:12 +00:00
res = s . DoRaw ( "POST" , fmt . Sprintf ( "/api/latest/fleet/hosts/%d/configuration_profiles/%s/resend" , host . ID , mcUUID ) , nil , http . StatusConflict )
errMsg = extractServerErrorText ( res . Body )
2024-04-18 21:01:37 +00:00
require . Contains ( t , errMsg , "Couldn’ t resend. Configuration profiles with “pending” or “verifying” status can’ t be resent." )
// set the profile to pending, can't resend
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
stmt := ` UPDATE host_mdm_apple_profiles SET status = ? WHERE profile_uuid = ? AND host_uuid = ? `
_ , err := q . ExecContext ( context . Background ( ) , stmt , fleet . MDMDeliveryPending , mcUUID , host . UUID )
return err
} )
s . checkMDMProfilesSummaries ( t , & tm . ID , fleet . MDMProfilesSummary { Pending : 1 } , nil )
2024-11-27 20:39:55 +00:00
res = s . DoRaw ( "POST" , fmt . Sprintf ( "/api/latest/fleet/hosts/%d/configuration_profiles/%s/resend" , host . ID , mcUUID ) , nil , http . StatusConflict )
2024-04-18 21:01:37 +00:00
errMsg = extractServerErrorText ( res . Body )
require . Contains ( t , errMsg , "Couldn’ t resend. Configuration profiles with “pending” or “verifying” status can’ t be resent." )
// set the profile to failed, can resend
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
stmt := ` UPDATE host_mdm_apple_profiles SET status = ? WHERE profile_uuid = ? AND host_uuid = ? `
_ , err := q . ExecContext ( context . Background ( ) , stmt , fleet . MDMDeliveryFailed , mcUUID , host . UUID )
return err
} )
s . checkMDMProfilesSummaries ( t , & tm . ID , fleet . MDMProfilesSummary { Failed : 1 } , nil )
2024-11-27 20:39:55 +00:00
_ = s . DoRaw ( "POST" , fmt . Sprintf ( "/api/latest/fleet/hosts/%d/configuration_profiles/%s/resend" , host . ID , mcUUID ) , nil , http . StatusAccepted )
2024-04-18 21:01:37 +00:00
s . awaitTriggerProfileSchedule ( t )
installs , removes = checkNextPayloads ( t , mdmDevice , false )
require . Len ( t , installs , 1 )
s . signedProfilesMatch ( [ ] [ ] byte { prof } , installs )
require . Empty ( t , removes )
s . checkMDMProfilesSummaries ( t , & tm . ID , fleet . MDMProfilesSummary { Verifying : 1 } , nil )
2025-09-16 07:25:02 +00:00
// set the profile to failed, can resend from device endpoint
token := "good_token"
updateDeviceTokenForHost ( t , s . ds , host . ID , token )
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
stmt := ` UPDATE host_mdm_apple_profiles SET status = ? WHERE profile_uuid = ? AND host_uuid = ? `
_ , err := q . ExecContext ( context . Background ( ) , stmt , fleet . MDMDeliveryFailed , mcUUID , host . UUID )
return err
} )
s . checkMDMProfilesSummaries ( t , & tm . ID , fleet . MDMProfilesSummary { Failed : 1 } , nil )
2025-09-18 06:38:52 +00:00
_ = s . DoRawNoAuth ( "POST" , fmt . Sprintf ( "/api/latest/fleet/device/%s/configuration_profiles/%s/resend" , token , mcUUID ) , nil , http . StatusAccepted )
2025-09-16 07:25:02 +00:00
s . awaitTriggerProfileSchedule ( t )
installs , removes = checkNextPayloads ( t , mdmDevice , false )
require . Len ( t , installs , 1 )
s . signedProfilesMatch ( [ ] [ ] byte { prof } , installs )
require . Empty ( t , removes )
s . checkMDMProfilesSummaries ( t , & tm . ID , fleet . MDMProfilesSummary { Verifying : 1 } , nil )
2024-04-18 21:01:37 +00:00
// can't resend profile while verifying
2024-11-27 20:39:55 +00:00
res = s . DoRaw ( "POST" , fmt . Sprintf ( "/api/latest/fleet/hosts/%d/configuration_profiles/%s/resend" , host . ID , mcUUID ) , nil , http . StatusConflict )
2024-04-18 21:01:37 +00:00
errMsg = extractServerErrorText ( res . Body )
require . Contains ( t , errMsg , "Couldn’ t resend. Configuration profiles with “pending” or “verifying” status can’ t be resent." )
// set the profile to verified, can resend
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
stmt := ` UPDATE host_mdm_apple_profiles SET status = ? WHERE profile_uuid = ? AND host_uuid = ? `
_ , err := q . ExecContext ( context . Background ( ) , stmt , fleet . MDMDeliveryVerified , mcUUID , host . UUID )
return err
} )
2024-11-27 20:39:55 +00:00
_ = s . DoRaw ( "POST" , fmt . Sprintf ( "/api/latest/fleet/hosts/%d/configuration_profiles/%s/resend" , host . ID , mcUUID ) , nil , http . StatusAccepted )
2024-04-18 21:01:37 +00:00
s . awaitTriggerProfileSchedule ( t )
installs , removes = checkNextPayloads ( t , mdmDevice , false )
require . Len ( t , installs , 1 )
s . signedProfilesMatch ( [ ] [ ] byte { prof } , installs )
require . Empty ( t , removes )
s . checkMDMProfilesSummaries ( t , & tm . ID , fleet . MDMProfilesSummary { Verifying : 1 } , nil )
s . lastActivityMatches (
fleet . ActivityTypeResentConfigurationProfile { } . ActivityName ( ) ,
fmt . Sprintf ( ` { "host_id": %d, "host_display_name": %q, "profile_name": %q} ` , host . ID , host . DisplayName ( ) , "name-" + mcUUID ) ,
0 )
// add a declaration to the team
declIdent := "decl-ident-" + uuid . NewString ( )
fields := map [ string ] [ ] string {
"team_id" : { fmt . Sprintf ( "%d" , tm . ID ) } ,
}
body , headers := generateNewProfileMultipartRequest (
t , "some-declaration.json" , declarationForTest ( declIdent ) , s . token , fields ,
)
res = s . DoRawWithHeaders ( "POST" , "/api/latest/fleet/configuration_profiles" , body . Bytes ( ) , http . StatusOK , headers )
var resp newMDMConfigProfileResponse
err = json . NewDecoder ( res . Body ) . Decode ( & resp )
require . NoError ( t , err )
require . NotEmpty ( t , resp . ProfileUUID )
require . Equal ( t , "d" , string ( resp . ProfileUUID [ 0 ] ) )
declUUID := resp . ProfileUUID
checkDDMSync := func ( d * mdmtest . TestAppleMDMClient ) {
require . NoError ( t , ReconcileAppleDeclarations ( ctx , s . ds , s . mdmCommander , s . logger ) )
cmd , err := d . Idle ( )
require . NoError ( t , err )
require . NotNil ( t , cmd )
require . Equal ( t , "DeclarativeManagement" , cmd . Command . RequestType )
cmd , err = d . Acknowledge ( cmd . CommandUUID )
require . NoError ( t , err )
require . Nil ( t , cmd , fmt . Sprintf ( "expected no more commands, but got: %+v" , cmd ) )
_ , err = d . DeclarativeManagement ( "tokens" )
require . NoError ( t , err )
}
checkDDMSync ( mdmDevice )
s . checkMDMProfilesSummaries ( t , & tm . ID , fleet . MDMProfilesSummary { Verifying : 1 } , nil )
2025-09-16 07:25:02 +00:00
// can not resend declarations as admin or from device endpoint
res = s . DoRaw ( "POST" , fmt . Sprintf ( "/api/latest/fleet/hosts/%d/configuration_profiles/%s/resend" , host . ID , declUUID ) , nil , http . StatusBadRequest )
2024-04-18 21:01:37 +00:00
errMsg = extractServerErrorText ( res . Body )
2025-09-16 07:25:02 +00:00
require . Contains ( t , errMsg , fleet . CantResendAppleDeclarationProfilesMessage )
2025-09-18 06:38:52 +00:00
res = s . DoRawNoAuth ( "POST" , fmt . Sprintf ( "/api/latest/fleet/device/%s/configuration_profiles/%s/resend" , token , declUUID ) , nil , http . StatusBadRequest )
2025-09-16 07:25:02 +00:00
errMsg = extractServerErrorText ( res . Body )
require . Contains ( t , errMsg , fleet . CantResendAppleDeclarationProfilesMessage )
2024-04-18 21:01:37 +00:00
2025-09-16 07:25:02 +00:00
// set the declaration to verified
2024-04-18 21:01:37 +00:00
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
stmt := ` UPDATE host_mdm_apple_declarations SET status = ? WHERE declaration_uuid = ? AND host_uuid = ? `
_ , err := q . ExecContext ( context . Background ( ) , stmt , fleet . MDMDeliveryVerified , declUUID , host . UUID )
return err
} )
// transfer the host to the global team
2025-07-17 14:20:49 +00:00
err = s . ds . AddHostsToTeam ( ctx , fleet . NewAddHostsToTeamParams ( nil , [ ] uint { host . ID } ) )
2024-04-18 21:01:37 +00:00
require . NoError ( t , err )
s . awaitTriggerProfileSchedule ( t )
installs , removes = checkNextPayloads ( t , mdmDevice , false )
require . Len ( t , installs , len ( wantGlobalProfiles ) )
s . signedProfilesMatch ( wantGlobalProfiles , installs )
require . Len ( t , removes , len ( wantTeamProfiles ) )
2025-07-16 16:03:16 +00:00
expectedNoTeamSummary = fleet . MDMProfilesSummary { Pending : 1 }
2024-04-18 21:01:37 +00:00
expectedTeamSummary = fleet . MDMProfilesSummary { }
2025-07-16 16:03:16 +00:00
s . checkMDMProfilesSummaries ( t , nil , expectedNoTeamSummary , & expectedNoTeamSummary ) // host now removing team profiles
2024-04-18 21:01:37 +00:00
s . checkMDMProfilesSummaries ( t , & tm . ID , expectedTeamSummary , & expectedTeamSummary )
// can't resend profile from another team
2024-11-27 20:39:55 +00:00
res = s . DoRaw ( "POST" , fmt . Sprintf ( "/api/latest/fleet/hosts/%d/configuration_profiles/%s/resend" , host . ID , mcUUID ) , nil , http . StatusNotFound )
2024-04-18 21:01:37 +00:00
errMsg = extractServerErrorText ( res . Body )
require . Contains ( t , errMsg , "Unable to match profile to host" )
// invalid profile UUID prefix should return 404
2024-11-27 20:39:55 +00:00
res = s . DoRaw ( "POST" , fmt . Sprintf ( "/api/latest/fleet/hosts/%d/configuration_profiles/%s/resend" , host . ID , "z" + uuid . NewString ( ) ) , nil , http . StatusNotFound )
2024-04-18 21:01:37 +00:00
errMsg = extractServerErrorText ( res . Body )
require . Contains ( t , errMsg , "Invalid profile UUID prefix" )
// set OS updates settings for no-team and team, should not change the
// summaries as this profile is ignored.
s . Do ( "PATCH" , "/api/latest/fleet/config" , json . RawMessage ( ` {
"mdm" : {
"macos_updates" : {
"deadline" : "2023-12-31" ,
"minimum_version" : "13.3.7"
}
}
} ` ) , http . StatusOK )
s . Do ( "PATCH" , fmt . Sprintf ( "/api/latest/fleet/teams/%d" , tm . ID ) , fleet . TeamPayload {
MDM : & fleet . TeamPayloadMDM {
2024-07-24 17:34:23 +00:00
MacOSUpdates : & fleet . AppleOSUpdateSettings {
2024-04-18 21:01:37 +00:00
Deadline : optjson . SetString ( "1992-01-01" ) ,
MinimumVersion : optjson . SetString ( "13.1.1" ) ,
} ,
} ,
} , http . StatusOK )
s . checkMDMProfilesSummaries ( t , nil , expectedNoTeamSummary , & expectedNoTeamSummary )
s . checkMDMProfilesSummaries ( t , & tm . ID , expectedTeamSummary , & expectedTeamSummary )
// it should also not show up in the host's profiles list
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/v1/fleet/hosts/%d" , host . ID ) , getHostRequest { } , http . StatusOK , & hostResp )
require . NotEmpty ( t , hostResp . Host . MDM . Profiles )
resProfiles = * hostResp . Host . MDM . Profiles
// two extra profiles: fleetd config and root CA
require . Len ( t , resProfiles , len ( wantGlobalProfiles ) + 2 )
}
func ( s * integrationMDMTestSuite ) TestAppleProfileRetries ( ) {
t := s . T ( )
ctx := context . Background ( )
enrollSecret := "test-profile-retries-secret"
err := s . ds . ApplyEnrollSecrets ( ctx , nil , [ ] * fleet . EnrollSecret { { Secret : enrollSecret } } )
require . NoError ( t , err )
testProfiles := [ ] [ ] byte {
mobileconfigForTest ( "N1" , "I1" ) ,
mobileconfigForTest ( "N2" , "I2" ) ,
}
2024-10-18 17:38:26 +00:00
initialExpectedProfiles := testProfiles
initialExpectedProfiles = append (
initialExpectedProfiles ,
2024-04-18 21:01:37 +00:00
setupExpectedFleetdProfile ( t , s . server . URL , enrollSecret , nil ) ,
2024-05-30 21:18:42 +00:00
setupExpectedCAProfile ( t , s . ds ) ,
2024-04-18 21:01:37 +00:00
)
h , mdmDevice := createHostThenEnrollMDM ( s . ds , s . server . URL , t )
setupPusher ( s , t , mdmDevice )
expectedProfileStatuses := map [ string ] fleet . MDMDeliveryStatus {
"I1" : fleet . MDMDeliveryVerifying ,
"I2" : fleet . MDMDeliveryVerifying ,
mobileconfig . FleetdConfigPayloadIdentifier : fleet . MDMDeliveryVerifying ,
mobileconfig . FleetCARootConfigPayloadIdentifier : fleet . MDMDeliveryVerifying ,
}
checkProfilesStatus := func ( t * testing . T ) {
storedProfs , err := s . ds . GetHostMDMAppleProfiles ( ctx , h . UUID )
require . NoError ( t , err )
require . Len ( t , storedProfs , len ( expectedProfileStatuses ) )
for _ , p := range storedProfs {
want , ok := expectedProfileStatuses [ p . Identifier ]
require . True ( t , ok , "unexpected profile: %s" , p . Identifier )
require . Equal ( t , want , * p . Status , "expected status %s but got %s for profile: %s" , want , * p . Status , p . Identifier )
}
}
expectedRetryCounts := map [ string ] uint {
"I1" : 0 ,
"I2" : 0 ,
mobileconfig . FleetdConfigPayloadIdentifier : 0 ,
mobileconfig . FleetCARootConfigPayloadIdentifier : 0 ,
}
checkRetryCounts := func ( t * testing . T ) {
counts , err := s . ds . GetHostMDMProfilesRetryCounts ( ctx , h )
require . NoError ( t , err )
require . Len ( t , counts , len ( expectedRetryCounts ) )
for _ , c := range counts {
want , ok := expectedRetryCounts [ c . ProfileIdentifier ]
require . True ( t , ok , "unexpected profile: %s" , c . ProfileIdentifier )
require . Equal ( t , want , c . Retries , "expected retry count %d but got %d for profile: %s" , want , c . Retries , c . ProfileIdentifier )
}
}
hostProfsByIdent := map [ string ] * fleet . HostMacOSProfile {
"I1" : {
Identifier : "I1" ,
DisplayName : "N1" ,
InstallDate : time . Now ( ) . Add ( 15 * time . Minute ) ,
} ,
"I2" : {
Identifier : "I2" ,
DisplayName : "N2" ,
InstallDate : time . Now ( ) . Add ( 15 * time . Minute ) ,
} ,
mobileconfig . FleetdConfigPayloadIdentifier : {
Identifier : mobileconfig . FleetdConfigPayloadIdentifier ,
DisplayName : "Fleetd configuration" ,
InstallDate : time . Now ( ) . Add ( 15 * time . Minute ) ,
} ,
}
reportHostProfs := func ( t * testing . T , identifiers ... string ) {
report := make ( map [ string ] * fleet . HostMacOSProfile , len ( hostProfsByIdent ) )
for _ , ident := range identifiers {
report [ ident ] = hostProfsByIdent [ ident ]
}
require . NoError ( t , apple_mdm . VerifyHostMDMProfiles ( ctx , s . ds , h , report ) )
}
setProfileUploadedAt := func ( t * testing . T , uploadedAt time . Time , identifiers ... interface { } ) {
bindVars := strings . TrimSuffix ( strings . Repeat ( "?, " , len ( identifiers ) ) , ", " )
stmt := fmt . Sprintf ( "UPDATE mdm_apple_configuration_profiles SET uploaded_at = ? WHERE identifier IN(%s)" , bindVars )
args := append ( [ ] interface { } { uploadedAt } , identifiers ... )
mysql . ExecAdhocSQL ( t , s . ds , func ( tx sqlx . ExtContext ) error {
_ , err := tx . ExecContext ( ctx , stmt , args ... )
return err
} )
}
t . Run ( "retry after verifying" , func ( t * testing . T ) {
// upload test profiles then simulate expired grace period by setting updated_at timestamp of profiles back by 48 hours
s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" , batchSetMDMAppleProfilesRequest { Profiles : testProfiles } , http . StatusNoContent )
setProfileUploadedAt ( t , time . Now ( ) . Add ( - 48 * time . Hour ) , "I1" , "I2" , mobileconfig . FleetdConfigPayloadIdentifier )
// trigger initial profile sync and confirm that we received all profiles
s . awaitTriggerProfileSchedule ( t )
installs , removes := checkNextPayloads ( t , mdmDevice , false )
s . signedProfilesMatch ( initialExpectedProfiles , installs )
require . Empty ( t , removes )
checkProfilesStatus ( t ) // all profiles verifying
checkRetryCounts ( t ) // no retries yet
// report osquery results with I2 missing and confirm I2 marked as pending and other profiles are marked as verified
reportHostProfs ( t , "I1" , mobileconfig . FleetdConfigPayloadIdentifier )
expectedProfileStatuses [ "I2" ] = fleet . MDMDeliveryPending
expectedProfileStatuses [ "I1" ] = fleet . MDMDeliveryVerified
expectedProfileStatuses [ mobileconfig . FleetdConfigPayloadIdentifier ] = fleet . MDMDeliveryVerified
checkProfilesStatus ( t )
expectedRetryCounts [ "I2" ] = 1
checkRetryCounts ( t )
// trigger a profile sync and confirm that the install profile command for I2 was resent
s . awaitTriggerProfileSchedule ( t )
installs , removes = checkNextPayloads ( t , mdmDevice , false )
s . signedProfilesMatch ( [ ] [ ] byte { initialExpectedProfiles [ 1 ] } , installs )
require . Empty ( t , removes )
// report osquery results with I2 present and confirm that all profiles are verified
reportHostProfs ( t , "I1" , "I2" , mobileconfig . FleetdConfigPayloadIdentifier )
expectedProfileStatuses [ "I2" ] = fleet . MDMDeliveryVerified
checkProfilesStatus ( t )
checkRetryCounts ( t ) // unchanged
// trigger a profile sync and confirm that no profiles were sent
s . awaitTriggerProfileSchedule ( t )
installs , removes = checkNextPayloads ( t , mdmDevice , false )
require . Empty ( t , installs )
require . Empty ( t , removes )
} )
t . Run ( "retry after verification" , func ( t * testing . T ) {
// report osquery results with I1 missing and confirm that the I1 marked as pending (initial retry)
reportHostProfs ( t , "I2" , mobileconfig . FleetdConfigPayloadIdentifier )
expectedProfileStatuses [ "I1" ] = fleet . MDMDeliveryPending
checkProfilesStatus ( t )
expectedRetryCounts [ "I1" ] = 1
checkRetryCounts ( t )
// trigger a profile sync and confirm that the install profile command for I1 was resent
s . awaitTriggerProfileSchedule ( t )
installs , removes := checkNextPayloads ( t , mdmDevice , false )
s . signedProfilesMatch ( [ ] [ ] byte { initialExpectedProfiles [ 0 ] } , installs )
require . Empty ( t , removes )
// report osquery results with I1 missing again and confirm that the I1 marked as failed (max retries exceeded)
reportHostProfs ( t , "I2" , mobileconfig . FleetdConfigPayloadIdentifier )
expectedProfileStatuses [ "I1" ] = fleet . MDMDeliveryFailed
checkProfilesStatus ( t )
checkRetryCounts ( t ) // unchanged
// trigger a profile sync and confirm that the install profile command for I1 was not resent
s . awaitTriggerProfileSchedule ( t )
installs , removes = checkNextPayloads ( t , mdmDevice , false )
require . Empty ( t , installs )
require . Empty ( t , removes )
} )
t . Run ( "retry after device error" , func ( t * testing . T ) {
// add another profile and set the updated_at timestamp back by 48 hours
newProfile := mobileconfigForTest ( "N3" , "I3" )
testProfiles = append ( testProfiles , newProfile )
s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" , batchSetMDMAppleProfilesRequest { Profiles : testProfiles } , http . StatusNoContent )
setProfileUploadedAt ( t , time . Now ( ) . Add ( - 48 * time . Hour ) , "I1" , "I2" , mobileconfig . FleetdConfigPayloadIdentifier , "I3" )
// trigger a profile sync and confirm that the install profile command for I3 was sent and
// simulate a device error
s . awaitTriggerProfileSchedule ( t )
installs , removes := checkNextPayloads ( t , mdmDevice , true )
s . signedProfilesMatch ( [ ] [ ] byte { newProfile } , installs )
require . Empty ( t , removes )
expectedProfileStatuses [ "I3" ] = fleet . MDMDeliveryPending
checkProfilesStatus ( t )
expectedRetryCounts [ "I3" ] = 1
checkRetryCounts ( t )
// trigger a profile sync and confirm that the install profile command for I3 was sent and
// simulate a device ack
s . awaitTriggerProfileSchedule ( t )
installs , removes = checkNextPayloads ( t , mdmDevice , false )
s . signedProfilesMatch ( [ ] [ ] byte { newProfile } , installs )
require . Empty ( t , removes )
expectedProfileStatuses [ "I3" ] = fleet . MDMDeliveryVerifying
checkProfilesStatus ( t )
checkRetryCounts ( t ) // unchanged
// report osquery results with I3 missing and confirm that the I3 marked as failed (max
// retries exceeded)
reportHostProfs ( t , "I2" , mobileconfig . FleetdConfigPayloadIdentifier )
expectedProfileStatuses [ "I3" ] = fleet . MDMDeliveryFailed
checkProfilesStatus ( t )
checkRetryCounts ( t ) // unchanged
// trigger a profile sync and confirm that the install profile command for I3 was not resent
s . awaitTriggerProfileSchedule ( t )
installs , removes = checkNextPayloads ( t , mdmDevice , false )
require . Empty ( t , installs )
require . Empty ( t , removes )
} )
t . Run ( "repeated device error" , func ( t * testing . T ) {
// add another profile and set the updated_at timestamp back by 48 hours
newProfile := mobileconfigForTest ( "N4" , "I4" )
testProfiles = append ( testProfiles , newProfile )
s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" , batchSetMDMAppleProfilesRequest { Profiles : testProfiles } , http . StatusNoContent )
setProfileUploadedAt ( t , time . Now ( ) . Add ( - 48 * time . Hour ) , "I1" , "I2" , mobileconfig . FleetdConfigPayloadIdentifier , "I3" , "I4" )
// trigger a profile sync and confirm that the install profile command for I3 was sent and
// simulate a device error
s . awaitTriggerProfileSchedule ( t )
installs , removes := checkNextPayloads ( t , mdmDevice , true )
s . signedProfilesMatch ( [ ] [ ] byte { newProfile } , installs )
require . Empty ( t , removes )
expectedProfileStatuses [ "I4" ] = fleet . MDMDeliveryPending
checkProfilesStatus ( t )
expectedRetryCounts [ "I4" ] = 1
checkRetryCounts ( t )
// trigger a profile sync and confirm that the install profile command for I4 was sent and
// simulate a second device error
s . awaitTriggerProfileSchedule ( t )
installs , removes = checkNextPayloads ( t , mdmDevice , true )
s . signedProfilesMatch ( [ ] [ ] byte { newProfile } , installs )
require . Empty ( t , removes )
expectedProfileStatuses [ "I4" ] = fleet . MDMDeliveryFailed
checkProfilesStatus ( t )
checkRetryCounts ( t ) // unchanged
// trigger a profile sync and confirm that the install profile command for I3 was not resent
s . awaitTriggerProfileSchedule ( t )
installs , removes = checkNextPayloads ( t , mdmDevice , false )
require . Empty ( t , installs )
require . Empty ( t , removes )
} )
t . Run ( "retry count does not reset" , func ( t * testing . T ) {
// add another profile and set the updated_at timestamp back by 48 hours
newProfile := mobileconfigForTest ( "N5" , "I5" )
testProfiles = append ( testProfiles , newProfile )
hostProfsByIdent [ "I5" ] = & fleet . HostMacOSProfile { Identifier : "I5" , DisplayName : "N5" , InstallDate : time . Now ( ) }
s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" , batchSetMDMAppleProfilesRequest { Profiles : testProfiles } , http . StatusNoContent )
setProfileUploadedAt ( t , time . Now ( ) . Add ( - 48 * time . Hour ) , "I1" , "I2" , mobileconfig . FleetdConfigPayloadIdentifier , "I3" , "I4" , "I5" )
// trigger a profile sync and confirm that the install profile command for I3 was sent and
// simulate a device error
s . awaitTriggerProfileSchedule ( t )
installs , removes := checkNextPayloads ( t , mdmDevice , true )
s . signedProfilesMatch ( [ ] [ ] byte { newProfile } , installs )
require . Empty ( t , removes )
expectedProfileStatuses [ "I5" ] = fleet . MDMDeliveryPending
checkProfilesStatus ( t )
expectedRetryCounts [ "I5" ] = 1
checkRetryCounts ( t )
// trigger a profile sync and confirm that the install profile command for I5 was sent and
// simulate a device ack
s . awaitTriggerProfileSchedule ( t )
installs , removes = checkNextPayloads ( t , mdmDevice , false )
s . signedProfilesMatch ( [ ] [ ] byte { newProfile } , installs )
require . Empty ( t , removes )
expectedProfileStatuses [ "I5" ] = fleet . MDMDeliveryVerifying
checkProfilesStatus ( t )
checkRetryCounts ( t ) // unchanged
// report osquery results with I5 found and confirm that the I5 marked as verified
reportHostProfs ( t , "I2" , mobileconfig . FleetdConfigPayloadIdentifier , "I5" )
expectedProfileStatuses [ "I5" ] = fleet . MDMDeliveryVerified
checkProfilesStatus ( t )
checkRetryCounts ( t ) // unchanged
// trigger a profile sync and confirm that the install profile command for I5 was not resent
s . awaitTriggerProfileSchedule ( t )
installs , removes = checkNextPayloads ( t , mdmDevice , false )
require . Empty ( t , installs )
require . Empty ( t , removes )
// report osquery results again, this time I5 is missing and confirm that the I5 marked as
// failed (max retries exceeded)
reportHostProfs ( t , "I2" , mobileconfig . FleetdConfigPayloadIdentifier )
expectedProfileStatuses [ "I5" ] = fleet . MDMDeliveryFailed
checkProfilesStatus ( t )
checkRetryCounts ( t ) // unchanged
// trigger a profile sync and confirm that the install profile command for I5 was not resent
s . awaitTriggerProfileSchedule ( t )
installs , removes = checkNextPayloads ( t , mdmDevice , false )
require . Empty ( t , installs )
require . Empty ( t , removes )
} )
}
2025-11-12 19:59:09 +00:00
type profileData struct {
Status string
LocURI string
Data string
}
// reportWindowsOSQueryProfiles simulates a Windows host reporting the status of MDM profiles from OSQuery results.
func ( s * integrationMDMTestSuite ) reportWindowsOSQueryProfiles ( ctx context . Context , t * testing . T , host * fleet . Host , hostProfileReports map [ string ] [ ] profileData ) {
var responseOps [ ] * fleet . SyncMLCmd
for profileName , report := range hostProfileReports {
for _ , p := range report {
ref := microsoft_mdm . HashLocURI ( profileName , p . LocURI )
responseOps = append ( responseOps , & fleet . SyncMLCmd {
XMLName : xml . Name { Local : fleet . CmdStatus } ,
CmdID : fleet . CmdID { Value : uuid . NewString ( ) } ,
CmdRef : & ref ,
Data : ptr . String ( p . Status ) ,
} )
// the protocol can respond with only a `Status`
// command if the status failed
if p . Status != "200" || p . Data != "" {
responseOps = append ( responseOps , & fleet . SyncMLCmd {
XMLName : xml . Name { Local : fleet . CmdResults } ,
CmdID : fleet . CmdID { Value : uuid . NewString ( ) } ,
CmdRef : & ref ,
Items : [ ] fleet . CmdItem {
{ Target : ptr . String ( p . LocURI ) , Data : & fleet . RawXmlData { Content : p . Data } } ,
} ,
} )
}
}
}
msg , err := createSyncMLMessage ( "2" , "2" , "foo" , "bar" , responseOps )
require . NoError ( t , err )
out , err := xml . Marshal ( msg )
require . NoError ( t , err )
require . NoError ( t , microsoft_mdm . VerifyHostMDMProfiles ( ctx , s . logger , s . ds , host , out ) )
}
2024-04-18 21:01:37 +00:00
func ( s * integrationMDMTestSuite ) TestWindowsProfileRetries ( ) {
t := s . T ( )
ctx := context . Background ( )
testProfiles := [ ] fleet . MDMProfileBatchPayload {
2025-04-18 12:45:18 +00:00
{ Name : "N1" , Contents : syncml . ForTestWithData ( [ ] syncml . TestCommand { { Verb : "Replace" , LocURI : "L1" , Data : "D1" } } ) } ,
{ Name : "N2" , Contents : syncml . ForTestWithData ( [ ] syncml . TestCommand { { Verb : "Replace" , LocURI : "L2" , Data : "D2" } , { Verb : "Add" , LocURI : "L3" , Data : "D3" } } ) } ,
2024-04-18 21:01:37 +00:00
}
h , mdmDevice := createWindowsHostThenEnrollMDM ( s . ds , s . server . URL , t )
expectedProfileStatuses := map [ string ] fleet . MDMDeliveryStatus {
"N1" : fleet . MDMDeliveryVerifying ,
"N2" : fleet . MDMDeliveryVerifying ,
}
checkProfilesStatus := func ( t * testing . T ) {
storedProfs , err := s . ds . GetHostMDMWindowsProfiles ( ctx , h . UUID )
require . NoError ( t , err )
require . Len ( t , storedProfs , len ( expectedProfileStatuses ) )
for _ , p := range storedProfs {
want , ok := expectedProfileStatuses [ p . Name ]
require . True ( t , ok , "unexpected profile: %s" , p . Name )
require . Equal ( t , want , * p . Status , "expected status %s but got %s for profile: %s" , want , * p . Status , p . Name )
}
}
expectedRetryCounts := map [ string ] uint {
"N1" : 0 ,
"N2" : 0 ,
}
checkRetryCounts := func ( t * testing . T ) {
counts , err := s . ds . GetHostMDMProfilesRetryCounts ( ctx , h )
require . NoError ( t , err )
require . Len ( t , counts , len ( expectedRetryCounts ) )
for _ , c := range counts {
want , ok := expectedRetryCounts [ c . ProfileName ]
require . True ( t , ok , "unexpected profile: %s" , c . ProfileName )
require . Equal ( t , want , c . Retries , "expected retry count %d but got %d for profile: %s" , want , c . Retries , c . ProfileName )
}
}
hostProfileReports := map [ string ] [ ] profileData {
"N1" : { { "200" , "L1" , "D1" } } ,
"N2" : { { "200" , "L2" , "D2" } , { "200" , "L3" , "D3" } } ,
}
2025-11-12 19:59:09 +00:00
reportHostProfs := func ( profileNames ... string ) {
selectedReports := make ( map [ string ] [ ] profileData )
for _ , name := range profileNames {
if reports , exists := hostProfileReports [ name ] ; exists {
selectedReports [ name ] = reports
2024-04-18 21:01:37 +00:00
}
}
2025-11-12 19:59:09 +00:00
s . reportWindowsOSQueryProfiles ( ctx , t , h , selectedReports )
2024-04-18 21:01:37 +00:00
}
verifyCommands := func ( wantProfileInstalls int , status string ) {
s . awaitTriggerProfileSchedule ( t )
cmds , err := mdmDevice . StartManagementSession ( )
require . NoError ( t , err )
// profile installs + 2 protocol commands acks
require . Len ( t , cmds , wantProfileInstalls + 2 )
msgID , err := mdmDevice . GetCurrentMsgID ( )
require . NoError ( t , err )
atomicCmds := 0
for _ , c := range cmds {
if c . Verb == "Atomic" {
atomicCmds ++
}
mdmDevice . AppendResponse ( fleet . SyncMLCmd {
XMLName : xml . Name { Local : fleet . CmdStatus } ,
MsgRef : & msgID ,
CmdRef : ptr . String ( c . Cmd . CmdID . Value ) ,
Cmd : ptr . String ( c . Verb ) ,
Data : ptr . String ( status ) ,
Items : nil ,
CmdID : fleet . CmdID { Value : uuid . NewString ( ) } ,
} )
}
require . Equal ( t , wantProfileInstalls , atomicCmds )
cmds , err = mdmDevice . SendResponse ( )
require . NoError ( t , err )
// the ack of the message should be the only returned command
require . Len ( t , cmds , 1 )
}
t . Run ( "retry after verifying" , func ( t * testing . T ) {
// upload test profiles then simulate expired grace period by setting updated_at timestamp of profiles back by 48 hours
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : testProfiles } , http . StatusNoContent )
// profiles to install + 2 boilerplate <Status>
verifyCommands ( len ( testProfiles ) , syncml . CmdStatusOK )
checkProfilesStatus ( t ) // all profiles verifying
checkRetryCounts ( t ) // no retries yet
// report osquery results with N2 missing and confirm N2 marked
// as verifying and other profiles are marked as verified
2025-11-12 19:59:09 +00:00
reportHostProfs ( "N1" )
2024-04-18 21:01:37 +00:00
expectedProfileStatuses [ "N2" ] = fleet . MDMDeliveryPending
expectedProfileStatuses [ "N1" ] = fleet . MDMDeliveryVerified
checkProfilesStatus ( t )
expectedRetryCounts [ "N2" ] = 1
checkRetryCounts ( t )
// report osquery results with N2 present and confirm that all profiles are verified
verifyCommands ( 1 , syncml . CmdStatusOK )
2025-11-12 19:59:09 +00:00
reportHostProfs ( "N1" , "N2" )
2024-04-18 21:01:37 +00:00
expectedProfileStatuses [ "N2" ] = fleet . MDMDeliveryVerified
checkProfilesStatus ( t )
checkRetryCounts ( t ) // unchanged
// trigger a profile sync and confirm that no profiles were sent
verifyCommands ( 0 , syncml . CmdStatusOK )
} )
t . Run ( "retry after verification" , func ( t * testing . T ) {
// report osquery results with N1 missing and confirm that the N1 marked as pending (initial retry)
2025-11-12 19:59:09 +00:00
reportHostProfs ( "N2" )
2024-04-18 21:01:37 +00:00
expectedProfileStatuses [ "N1" ] = fleet . MDMDeliveryPending
checkProfilesStatus ( t )
expectedRetryCounts [ "N1" ] = 1
checkRetryCounts ( t )
// trigger a profile sync and confirm that the install profile command for N1 was resent
verifyCommands ( 1 , syncml . CmdStatusOK )
// report osquery results with N1 missing again and confirm that the N1 marked as failed (max retries exceeded)
2025-11-12 19:59:09 +00:00
reportHostProfs ( "N2" )
2024-04-18 21:01:37 +00:00
expectedProfileStatuses [ "N1" ] = fleet . MDMDeliveryFailed
checkProfilesStatus ( t )
checkRetryCounts ( t ) // unchanged
// trigger a profile sync and confirm that the install profile command for N1 was not resent
verifyCommands ( 0 , syncml . CmdStatusOK )
} )
t . Run ( "retry after device error" , func ( t * testing . T ) {
// add another profile
2025-04-18 12:45:18 +00:00
newProfile := syncml . ForTestWithData ( [ ] syncml . TestCommand { { Verb : "Replace" , LocURI : "L3" , Data : "D3" } } )
2024-04-18 21:01:37 +00:00
testProfiles = append ( testProfiles , fleet . MDMProfileBatchPayload {
Name : "N3" ,
Contents : newProfile ,
} )
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : testProfiles } , http . StatusNoContent )
// trigger a profile sync and confirm that the install profile command for N3 was sent and
// simulate a device error
verifyCommands ( 1 , syncml . CmdStatusAtomicFailed )
expectedProfileStatuses [ "N3" ] = fleet . MDMDeliveryPending
checkProfilesStatus ( t )
expectedRetryCounts [ "N3" ] = 1
checkRetryCounts ( t )
// trigger a profile sync and confirm that the install profile command for N3 was sent and
// simulate a device ack
verifyCommands ( 1 , syncml . CmdStatusOK )
expectedProfileStatuses [ "N3" ] = fleet . MDMDeliveryVerifying
checkProfilesStatus ( t )
checkRetryCounts ( t ) // unchanged
// report osquery results with N3 missing and confirm that the N3 marked as failed (max
// retries exceeded)
2025-11-12 19:59:09 +00:00
reportHostProfs ( "N2" )
2024-04-18 21:01:37 +00:00
expectedProfileStatuses [ "N3" ] = fleet . MDMDeliveryFailed
checkProfilesStatus ( t )
checkRetryCounts ( t ) // unchanged
// trigger a profile sync and confirm that the install profile command for N3 was not resent
verifyCommands ( 0 , syncml . CmdStatusOK )
} )
t . Run ( "repeated device error" , func ( t * testing . T ) {
// add another profile
testProfiles = append ( testProfiles , fleet . MDMProfileBatchPayload {
Name : "N4" ,
2025-04-18 12:45:18 +00:00
Contents : syncml . ForTestWithData ( [ ] syncml . TestCommand { { Verb : "Replace" , LocURI : "L4" , Data : "D4" } } ) ,
2024-04-18 21:01:37 +00:00
} )
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : testProfiles } , http . StatusNoContent )
// trigger a profile sync and confirm that the install profile command for N4 was sent and
// simulate a device error
verifyCommands ( 1 , syncml . CmdStatusAtomicFailed )
expectedProfileStatuses [ "N4" ] = fleet . MDMDeliveryPending
checkProfilesStatus ( t )
expectedRetryCounts [ "N4" ] = 1
checkRetryCounts ( t )
// trigger a profile sync and confirm that the install profile
// command for N4 was sent and simulate a second device error
verifyCommands ( 1 , syncml . CmdStatusAtomicFailed )
expectedProfileStatuses [ "N4" ] = fleet . MDMDeliveryFailed
checkProfilesStatus ( t )
checkRetryCounts ( t ) // unchanged
// trigger a profile sync and confirm that the install profile
// command for N4 was not resent
verifyCommands ( 0 , syncml . CmdStatusOK )
} )
t . Run ( "retry count does not reset" , func ( t * testing . T ) {
// add another profile
testProfiles = append ( testProfiles , fleet . MDMProfileBatchPayload {
Name : "N5" ,
2025-04-18 12:45:18 +00:00
Contents : syncml . ForTestWithData ( [ ] syncml . TestCommand { { Verb : "Replace" , LocURI : "L5" , Data : "D5" } } ) ,
2024-04-18 21:01:37 +00:00
} )
// hostProfsByIdent["N5"] = &fleet.HostMacOSProfile{Identifier: "N5", DisplayName: "N5", InstallDate: time.Now()}
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : testProfiles } , http . StatusNoContent )
// trigger a profile sync and confirm that the install profile
// command for N5 was sent and simulate a device error
verifyCommands ( 1 , syncml . CmdStatusAtomicFailed )
expectedProfileStatuses [ "N5" ] = fleet . MDMDeliveryPending
checkProfilesStatus ( t )
expectedRetryCounts [ "N5" ] = 1
checkRetryCounts ( t )
// trigger a profile sync and confirm that the install profile
// command for N5 was sent and simulate a device ack
verifyCommands ( 1 , syncml . CmdStatusOK )
expectedProfileStatuses [ "N5" ] = fleet . MDMDeliveryVerifying
checkProfilesStatus ( t )
checkRetryCounts ( t ) // unchanged
// report osquery results with N5 found and confirm that the N5 marked as verified
hostProfileReports [ "N5" ] = [ ] profileData { { "200" , "L5" , "D5" } }
2025-11-12 19:59:09 +00:00
reportHostProfs ( "N2" , "N5" )
2024-04-18 21:01:37 +00:00
expectedProfileStatuses [ "N5" ] = fleet . MDMDeliveryVerified
checkProfilesStatus ( t )
checkRetryCounts ( t ) // unchanged
// trigger a profile sync and confirm that the install profile command for N5 was not resent
verifyCommands ( 0 , syncml . CmdStatusOK )
// report osquery results again, this time N5 is missing and confirm that the N5 marked as
// failed (max retries exceeded)
2025-11-12 19:59:09 +00:00
reportHostProfs ( "N2" )
2024-04-18 21:01:37 +00:00
expectedProfileStatuses [ "N5" ] = fleet . MDMDeliveryFailed
checkProfilesStatus ( t )
checkRetryCounts ( t ) // unchanged
// trigger a profile sync and confirm that the install profile command for N5 was not resent
verifyCommands ( 0 , syncml . CmdStatusOK )
} )
}
2025-03-20 20:13:55 +00:00
// TestWindowsProfileResend verifies that a Windows profile is resent when its contents have been modified.
2025-03-20 19:43:04 +00:00
func ( s * integrationMDMTestSuite ) TestWindowsProfileResend ( ) {
t := s . T ( )
ctx := context . Background ( )
testProfiles := [ ] fleet . MDMProfileBatchPayload {
2025-04-18 12:45:18 +00:00
{ Name : "N1" , Contents : syncml . ForTestWithData ( [ ] syncml . TestCommand { { Verb : "Replace" , LocURI : "L1" , Data : "D1" } } ) } ,
{ Name : "N2" , Contents : syncml . ForTestWithData ( [ ] syncml . TestCommand { { Verb : "Replace" , LocURI : "L2" , Data : "D2" } , { Verb : "Replace" , LocURI : "L3" , Data : "D3" } } ) } ,
2025-03-20 19:43:04 +00:00
}
h , mdmDevice := createWindowsHostThenEnrollMDM ( s . ds , s . server . URL , t )
expectedProfileStatuses := map [ string ] fleet . MDMDeliveryStatus {
"N1" : fleet . MDMDeliveryVerifying ,
"N2" : fleet . MDMDeliveryVerifying ,
}
checkProfilesStatus := func ( t * testing . T ) {
storedProfs , err := s . ds . GetHostMDMWindowsProfiles ( ctx , h . UUID )
require . NoError ( t , err )
require . Len ( t , storedProfs , len ( expectedProfileStatuses ) )
for _ , p := range storedProfs {
want , ok := expectedProfileStatuses [ p . Name ]
require . True ( t , ok , "unexpected profile: %s" , p . Name )
require . Equal ( t , want , * p . Status , "expected status %s but got %s for profile: %s" , want , * p . Status , p . Name )
}
}
hostProfileReports := map [ string ] [ ] profileData {
"N1" : { { "200" , "L1" , "D1" } } ,
"N2" : { { "200" , "L2" , "D2" } , { "200" , "L3" , "D3" } } ,
}
2025-11-12 19:59:09 +00:00
reportHostProfs := func ( profileNames ... string ) {
selectedReports := make ( map [ string ] [ ] profileData )
for _ , name := range profileNames {
if reports , exists := hostProfileReports [ name ] ; exists {
selectedReports [ name ] = reports
2025-03-20 19:43:04 +00:00
}
}
2025-11-12 19:59:09 +00:00
s . reportWindowsOSQueryProfiles ( ctx , t , h , selectedReports )
2025-03-20 19:43:04 +00:00
}
verifyCommands := func ( wantProfileInstalls int , status string ) {
s . awaitTriggerProfileSchedule ( t )
cmds , err := mdmDevice . StartManagementSession ( )
require . NoError ( t , err )
// profile installs + 2 protocol commands acks
require . Len ( t , cmds , wantProfileInstalls + 2 )
msgID , err := mdmDevice . GetCurrentMsgID ( )
require . NoError ( t , err )
atomicCmds := 0
for _ , c := range cmds {
if c . Verb == "Atomic" {
atomicCmds ++
}
mdmDevice . AppendResponse ( fleet . SyncMLCmd {
XMLName : xml . Name { Local : fleet . CmdStatus } ,
MsgRef : & msgID ,
CmdRef : ptr . String ( c . Cmd . CmdID . Value ) ,
Cmd : ptr . String ( c . Verb ) ,
Data : ptr . String ( status ) ,
Items : nil ,
CmdID : fleet . CmdID { Value : uuid . NewString ( ) } ,
} )
}
require . Equal ( t , wantProfileInstalls , atomicCmds )
cmds , err = mdmDevice . SendResponse ( )
require . NoError ( t , err )
// the ack of the message should be the only returned command
require . Len ( t , cmds , 1 )
}
t . Run ( "do not resend if nothing changed" , func ( t * testing . T ) {
t . Cleanup ( func ( ) {
// Clear the profiles
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload { } } ,
http . StatusNoContent )
} )
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : testProfiles } , http . StatusNoContent )
// profiles to install + 2 boilerplate <Status>
verifyCommands ( len ( testProfiles ) , syncml . CmdStatusOK )
expectedProfileStatuses [ "N1" ] = fleet . MDMDeliveryVerifying
expectedProfileStatuses [ "N2" ] = fleet . MDMDeliveryVerifying
checkProfilesStatus ( t ) // all profiles verifying
// report osquery results and confirm that all profiles are verified
2025-11-12 19:59:09 +00:00
reportHostProfs ( "N1" , "N2" )
2025-03-20 19:43:04 +00:00
expectedProfileStatuses [ "N1" ] = fleet . MDMDeliveryVerified
expectedProfileStatuses [ "N2" ] = fleet . MDMDeliveryVerified
checkProfilesStatus ( t )
// trigger a profile sync and confirm that no profiles were sent
verifyCommands ( 0 , syncml . CmdStatusOK )
// Upload the same profiles again
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : testProfiles } , http . StatusNoContent )
// trigger a profile sync and confirm that no profiles were sent
verifyCommands ( 0 , syncml . CmdStatusOK )
} )
t . Run ( "resend if contents changed" , func ( t * testing . T ) {
t . Cleanup ( func ( ) {
// Clear the profiles
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload { } } ,
http . StatusNoContent )
} )
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : testProfiles } , http . StatusNoContent )
// profiles to install + 2 boilerplate <Status>
verifyCommands ( len ( testProfiles ) , syncml . CmdStatusOK )
expectedProfileStatuses [ "N1" ] = fleet . MDMDeliveryVerifying
expectedProfileStatuses [ "N2" ] = fleet . MDMDeliveryVerifying
checkProfilesStatus ( t ) // all profiles verifying
// report osquery results and confirm that all profiles are verified
2025-11-12 19:59:09 +00:00
reportHostProfs ( "N1" , "N2" )
2025-03-20 19:43:04 +00:00
expectedProfileStatuses [ "N1" ] = fleet . MDMDeliveryVerified
expectedProfileStatuses [ "N2" ] = fleet . MDMDeliveryVerified
checkProfilesStatus ( t )
// trigger a profile sync and confirm that no profiles were sent
verifyCommands ( 0 , syncml . CmdStatusOK )
// Change one profile and upload
copiedTestProfiles := make ( [ ] fleet . MDMProfileBatchPayload , len ( testProfiles ) )
copy ( copiedTestProfiles , testProfiles )
2025-04-18 12:45:18 +00:00
copiedTestProfiles [ 0 ] . Contents = syncml . ForTestWithData ( [ ] syncml . TestCommand { { Verb : "Replace" , LocURI : "L1" , Data : "D1-Modified" } } )
2025-03-20 19:43:04 +00:00
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : copiedTestProfiles } , http . StatusNoContent )
// Confirm that one profile was sent and its status
verifyCommands ( 1 , syncml . CmdStatusOK )
expectedProfileStatuses [ "N1" ] = fleet . MDMDeliveryVerifying
expectedProfileStatuses [ "N2" ] = fleet . MDMDeliveryVerified
checkProfilesStatus ( t ) // all profiles verifying
} )
}
2024-04-18 21:01:37 +00:00
func ( s * integrationMDMTestSuite ) TestPuppetMatchPreassignProfiles ( ) {
ctx := context . Background ( )
t := s . T ( )
2024-09-05 17:10:35 +00:00
// before we switch to a gitops token, ensure ABM is setup
s . enableABM ( t . Name ( ) )
2024-04-18 21:01:37 +00:00
// Use a gitops user for all Puppet actions
u := & fleet . User {
Name : "GitOps" ,
Email : "gitops-TestPuppetMatchPreassignProfiles@example.com" ,
GlobalRole : ptr . String ( fleet . RoleGitOps ) ,
}
require . NoError ( t , u . SetPassword ( test . GoodPassword , 10 , 10 ) )
_ , err := s . ds . NewUser ( context . Background ( ) , u )
require . NoError ( t , err )
s . setTokenForTest ( t , "gitops-TestPuppetMatchPreassignProfiles@example.com" , test . GoodPassword )
runWithAdminToken := func ( cb func ( ) ) {
s . token = s . getTestAdminToken ( )
cb ( )
s . token = s . getCachedUserToken ( "gitops-TestPuppetMatchPreassignProfiles@example.com" , test . GoodPassword )
}
// create a host enrolled in fleet
mdmHost , _ := createHostThenEnrollMDM ( s . ds , s . server . URL , t )
// create a host that's not enrolled into MDM
nonMDMHost , err := s . ds . NewHost ( context . Background ( ) , & fleet . Host {
OsqueryHostID : ptr . String ( "not-mdm-enrolled" ) ,
NodeKey : ptr . String ( "not-mdm-enrolled" ) ,
UUID : uuid . New ( ) . String ( ) ,
Hostname : fmt . Sprintf ( "%sfoo.local.not.enrolled" , t . Name ( ) ) ,
Platform : "darwin" ,
} )
require . NoError ( t , err )
// create a setup assistant for no team, for this we need to:
// 1. mock the ABM API, as it gets called to set the profile
// 2. run the DEP schedule, as this registers the default profile
2024-09-05 17:10:35 +00:00
s . mockDEPResponse ( t . Name ( ) , http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
2024-04-18 21:01:37 +00:00
w . WriteHeader ( http . StatusOK )
_ , _ = w . Write ( [ ] byte ( ` { "auth_session_token": "xyz"} ` ) )
} ) )
s . runDEPSchedule ( )
noTeamProf := ` { "x": 1} `
var globalAsstResp createMDMAppleSetupAssistantResponse
s . DoJSON ( "POST" , "/api/latest/fleet/enrollment_profiles/automatic" , createMDMAppleSetupAssistantRequest {
TeamID : nil ,
Name : "no-team" ,
EnrollmentProfile : json . RawMessage ( noTeamProf ) ,
} , http . StatusOK , & globalAsstResp )
// set the global Enable Release Device manually setting to true,
// will be inherited by teams created via preassign/match.
s . Do ( "PATCH" , "/api/latest/fleet/setup_experience" ,
json . RawMessage ( jsonMustMarshal ( t , map [ string ] any { "enable_release_device_manually" : true } ) ) ,
http . StatusNoContent )
s . runWorker ( )
// preassign an empty profile, fails
s . Do ( "POST" , "/api/latest/fleet/mdm/apple/profiles/preassign" , preassignMDMAppleProfileRequest { MDMApplePreassignProfilePayload : fleet . MDMApplePreassignProfilePayload { ExternalHostIdentifier : "empty" , HostUUID : nonMDMHost . UUID , Profile : nil } } , http . StatusUnprocessableEntity )
// preassign a valid profile to the MDM host
prof1 := mobileconfigForTest ( "n1" , "i1" )
s . Do ( "POST" , "/api/latest/fleet/mdm/apple/profiles/preassign" , preassignMDMAppleProfileRequest { MDMApplePreassignProfilePayload : fleet . MDMApplePreassignProfilePayload { ExternalHostIdentifier : "mdm1" , HostUUID : mdmHost . UUID , Profile : prof1 } } , http . StatusNoContent )
// preassign another valid profile to the MDM host
prof2 := mobileconfigForTest ( "n2" , "i2" )
s . Do ( "POST" , "/api/latest/fleet/mdm/apple/profiles/preassign" , preassignMDMAppleProfileRequest { MDMApplePreassignProfilePayload : fleet . MDMApplePreassignProfilePayload { ExternalHostIdentifier : "mdm1" , HostUUID : mdmHost . UUID , Profile : prof2 , Group : "g1" } } , http . StatusNoContent )
// preassign a valid profile to the non-MDM host, still works as the host is not validated in this call
prof3 := mobileconfigForTest ( "n3" , "i3" )
s . Do ( "POST" , "/api/latest/fleet/mdm/apple/profiles/preassign" , preassignMDMAppleProfileRequest { MDMApplePreassignProfilePayload : fleet . MDMApplePreassignProfilePayload { ExternalHostIdentifier : "non-mdm" , HostUUID : nonMDMHost . UUID , Profile : prof3 , Group : "g2" } } , http . StatusNoContent )
// match with an invalid external host id, succeeds as it is the same as if
// there was no matching to do (no preassignment was done)
s . Do ( "POST" , "/api/latest/fleet/mdm/apple/profiles/match" , matchMDMApplePreassignmentRequest { ExternalHostIdentifier : "no-such-id" } , http . StatusNoContent )
// match with the non-mdm host fails
res := s . Do ( "POST" , "/api/latest/fleet/mdm/apple/profiles/match" , matchMDMApplePreassignmentRequest { ExternalHostIdentifier : "non-mdm" } , http . StatusBadRequest )
errMsg := extractServerErrorText ( res . Body )
require . Contains ( t , errMsg , "host is not enrolled in Fleet MDM" )
// match with the mdm host succeeds and creates a team based on the group labels
s . Do ( "POST" , "/api/latest/fleet/mdm/apple/profiles/match" , matchMDMApplePreassignmentRequest { ExternalHostIdentifier : "mdm1" } , http . StatusNoContent )
// the host is now part of that team
h , err := s . ds . Host ( ctx , mdmHost . ID )
require . NoError ( t , err )
require . NotNil ( t , h . TeamID )
2025-11-17 23:25:45 +00:00
tm1 , err := s . ds . TeamLite ( ctx , * h . TeamID )
2024-04-18 21:01:37 +00:00
require . NoError ( t , err )
require . Equal ( t , "g1" , tm1 . Name )
require . True ( t , tm1 . Config . MDM . EnableDiskEncryption )
require . True ( t , tm1 . Config . MDM . MacOSSetup . EnableReleaseDeviceManually . Value )
runWithAdminToken ( func ( ) {
// it create activities for the new team, the profiles assigned to it,
// the host moved to it, and setup assistant
s . lastActivityOfTypeMatches (
fleet . ActivityTypeCreatedTeam { } . ActivityName ( ) ,
fmt . Sprintf ( ` { "team_id": %d, "team_name": %q} ` , tm1 . ID , tm1 . Name ) ,
0 )
s . lastActivityOfTypeMatches (
fleet . ActivityTypeEditedMacosProfile { } . ActivityName ( ) ,
fmt . Sprintf ( ` { "team_id": %d, "team_name": %q} ` , tm1 . ID , tm1 . Name ) ,
0 )
s . lastActivityOfTypeMatches (
fleet . ActivityTypeTransferredHostsToTeam { } . ActivityName ( ) ,
fmt . Sprintf ( ` { "team_id": %d, "team_name": %q, "host_ids": [%d], "host_display_names": [%q]} ` ,
tm1 . ID , tm1 . Name , h . ID , h . DisplayName ( ) ) ,
0 )
s . lastActivityOfTypeMatches (
fleet . ActivityTypeChangedMacosSetupAssistant { } . ActivityName ( ) ,
fmt . Sprintf ( ` { "team_id": %d, "name": %q, "team_name": %q} ` ,
tm1 . ID , globalAsstResp . Name , tm1 . Name ) ,
0 )
} )
// and the team has the expected profiles (prof1 and prof2)
profs , err := s . ds . ListMDMAppleConfigProfiles ( ctx , & tm1 . ID )
require . NoError ( t , err )
require . Len ( t , profs , 2 )
// order is guaranteed by profile name
require . Equal ( t , prof1 , [ ] byte ( profs [ 0 ] . Mobileconfig ) )
require . Equal ( t , prof2 , [ ] byte ( profs [ 1 ] . Mobileconfig ) )
// setup assistant settings are copyied from "no team"
teamAsst , err := s . ds . GetMDMAppleSetupAssistant ( ctx , & tm1 . ID )
require . NoError ( t , err )
require . Equal ( t , globalAsstResp . Name , teamAsst . Name )
require . JSONEq ( t , string ( globalAsstResp . Profile ) , string ( teamAsst . Profile ) )
// trigger the schedule so profiles are set in their state
s . awaitTriggerProfileSchedule ( t )
s . runWorker ( )
// the mdm host has the same profiles (i1, i2, plus fleetd config and disk encryption)
2024-07-03 14:20:33 +00:00
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
2024-04-18 21:01:37 +00:00
mdmHost : {
{ Identifier : "i1" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "i2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetFileVaultPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
} ,
} )
// create a team and set profiles to it (note that it doesn't have disk encryption enabled)
tm2 , err := s . ds . NewTeam ( context . Background ( ) , & fleet . Team {
Name : "g1 - g4" ,
Secrets : [ ] * fleet . EnrollSecret { { Secret : "tm2secret" } } ,
} )
require . NoError ( t , err )
prof4 := mobileconfigForTest ( "n4" , "i4" )
s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" , batchSetMDMAppleProfilesRequest { Profiles : [ ] [ ] byte {
prof1 , prof4 ,
} } , http . StatusNoContent , "team_id" , fmt . Sprint ( tm2 . ID ) )
// tm2 has disk encryption and release device manually disabled
require . False ( t , tm2 . Config . MDM . EnableDiskEncryption )
require . False ( t , tm2 . Config . MDM . MacOSSetup . EnableReleaseDeviceManually . Value )
// create another team with a superset of profiles
tm3 , err := s . ds . NewTeam ( context . Background ( ) , & fleet . Team {
Name : "team3_" + t . Name ( ) ,
Secrets : [ ] * fleet . EnrollSecret { { Secret : "tm3secret" } } ,
} )
require . NoError ( t , err )
s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" , batchSetMDMAppleProfilesRequest { Profiles : [ ] [ ] byte {
prof1 , prof2 , prof4 ,
} } , http . StatusNoContent , "team_id" , fmt . Sprint ( tm3 . ID ) )
// and yet another team with the same profiles as tm3
tm4 , err := s . ds . NewTeam ( context . Background ( ) , & fleet . Team {
Name : "team4_" + t . Name ( ) ,
Secrets : [ ] * fleet . EnrollSecret { { Secret : "tm4secret" } } ,
} )
require . NoError ( t , err )
s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" , batchSetMDMAppleProfilesRequest { Profiles : [ ] [ ] byte {
prof1 , prof2 , prof4 ,
} } , http . StatusNoContent , "team_id" , fmt . Sprint ( tm4 . ID ) )
// preassign the MDM host to prof1 and prof4, should match existing team tm2
//
// additionally, use external host identifiers with different
// suffixes to simulate real world distributed scenarios where more
// than one puppet server might be running at the time.
s . Do ( "POST" , "/api/latest/fleet/mdm/apple/profiles/preassign" , preassignMDMAppleProfileRequest { MDMApplePreassignProfilePayload : fleet . MDMApplePreassignProfilePayload { ExternalHostIdentifier : "6f36ab2c-1a40-429b-9c9d-07c9029f4aa8-puppetcompiler06.test.example.com" , HostUUID : mdmHost . UUID , Profile : prof1 , Group : "g1" } } , http . StatusNoContent )
s . Do ( "POST" , "/api/latest/fleet/mdm/apple/profiles/preassign" , preassignMDMAppleProfileRequest { MDMApplePreassignProfilePayload : fleet . MDMApplePreassignProfilePayload { ExternalHostIdentifier : "6f36ab2c-1a40-429b-9c9d-07c9029f4aa8-puppetcompiler01.test.example.com" , HostUUID : mdmHost . UUID , Profile : prof4 , Group : "g4" } } , http . StatusNoContent )
// match with the mdm host succeeds and assigns it to tm2
s . Do ( "POST" , "/api/latest/fleet/mdm/apple/profiles/match" , matchMDMApplePreassignmentRequest { ExternalHostIdentifier : "6f36ab2c-1a40-429b-9c9d-07c9029f4aa8-puppetcompiler03.test.example.com" } , http . StatusNoContent )
// the host is now part of that team
h , err = s . ds . Host ( ctx , mdmHost . ID )
require . NoError ( t , err )
require . NotNil ( t , h . TeamID )
require . Equal ( t , tm2 . ID , * h . TeamID )
2025-11-17 23:25:45 +00:00
// tmLite2 still has disk encryption and release device manually disabled
tmLite2 , err := s . ds . TeamLite ( ctx , * h . TeamID )
2024-04-18 21:01:37 +00:00
require . NoError ( t , err )
2025-11-17 23:25:45 +00:00
require . False ( t , tmLite2 . Config . MDM . EnableDiskEncryption )
require . False ( t , tmLite2 . Config . MDM . MacOSSetup . EnableReleaseDeviceManually . Value )
2024-04-18 21:01:37 +00:00
// the host's profiles are:
// - the same as the team's and are pending (prof1 + prof4)
// - prof2 + old filevault are pending removal
// - fleetd config being reinstalled (for new enroll secret)
s . awaitTriggerProfileSchedule ( t )
// useful for debugging
2024-09-19 15:31:46 +00:00
// mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
// mysql.DumpTable(t, q, "host_mdm_apple_profiles")
// return nil
// })
2024-07-03 14:20:33 +00:00
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
2024-04-18 21:01:37 +00:00
mdmHost : {
{ Identifier : "i1" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "i4" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
2025-01-06 19:16:34 +00:00
// Profiles from previous team being deleted
{ Identifier : "i2" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
2025-01-30 11:17:36 +00:00
{
Identifier : mobileconfig . FleetFileVaultPayloadIdentifier , OperationType : fleet . MDMOperationTypeRemove ,
Status : & fleet . MDMDeliveryPending ,
} ,
2024-04-18 21:01:37 +00:00
} ,
} )
// create a new mdm host enrolled in fleet
mdmHost2 , _ := createHostThenEnrollMDM ( s . ds , s . server . URL , t )
// make it part of team 2
s . Do ( "POST" , "/api/v1/fleet/hosts/transfer" ,
2025-11-17 23:25:45 +00:00
addHostsToTeamRequest { TeamID : & tmLite2 . ID , HostIDs : [ ] uint { mdmHost2 . ID } } , http . StatusOK )
2024-04-18 21:01:37 +00:00
// simulate having its profiles installed
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
res , err := q . ExecContext ( ctx , ` UPDATE host_mdm_apple_profiles SET status = ? WHERE host_uuid = ? ` , fleet . OSSettingsVerifying , mdmHost2 . UUID )
n , _ := res . RowsAffected ( )
require . Equal ( t , 4 , int ( n ) )
return err
} )
// preassign the MDM host using "g1" and "g4", should match existing
2025-11-17 23:25:45 +00:00
// team tmLite2, and nothing be done since the host is already in tmLite2
2024-04-18 21:01:37 +00:00
s . Do ( "POST" , "/api/latest/fleet/mdm/apple/profiles/preassign" , preassignMDMAppleProfileRequest { MDMApplePreassignProfilePayload : fleet . MDMApplePreassignProfilePayload { ExternalHostIdentifier : "mdm2" , HostUUID : mdmHost2 . UUID , Profile : prof1 , Group : "g1" } } , http . StatusNoContent )
s . Do ( "POST" , "/api/latest/fleet/mdm/apple/profiles/preassign" , preassignMDMAppleProfileRequest { MDMApplePreassignProfilePayload : fleet . MDMApplePreassignProfilePayload { ExternalHostIdentifier : "mdm2" , HostUUID : mdmHost2 . UUID , Profile : prof4 , Group : "g4" } } , http . StatusNoContent )
s . Do ( "POST" , "/api/latest/fleet/mdm/apple/profiles/match" , matchMDMApplePreassignmentRequest { ExternalHostIdentifier : "mdm2" } , http . StatusNoContent )
2025-11-17 23:25:45 +00:00
// the host is still part of tmLite2
2024-04-18 21:01:37 +00:00
h , err = s . ds . Host ( ctx , mdmHost2 . ID )
require . NoError ( t , err )
require . NotNil ( t , h . TeamID )
2025-11-17 23:25:45 +00:00
require . Equal ( t , tmLite2 . ID , * h . TeamID )
2024-04-18 21:01:37 +00:00
// and its profiles have been left untouched
s . awaitTriggerProfileSchedule ( t )
2024-07-03 14:20:33 +00:00
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
2024-04-18 21:01:37 +00:00
mdmHost2 : {
{ Identifier : "i1" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : "i4" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
} )
}
// while s.TestPuppetMatchPreassignProfiles focuses on many edge cases/extra
// checks around profile assignment, this test is mainly focused on
// simulating a few puppet runs in scenarios we want to support, and ensuring that:
//
// - different hosts end up in the right teams
// - teams get edited as expected
// - commands to add/remove profiles are issued adequately
func ( s * integrationMDMTestSuite ) TestPuppetRun ( ) {
t := s . T ( )
ctx := context . Background ( )
// define a few profiles
prof1 , prof2 , prof3 , prof4 := mobileconfigForTest ( "n1" , "i1" ) ,
mobileconfigForTest ( "n2" , "i2" ) ,
mobileconfigForTest ( "n3" , "i3" ) ,
mobileconfigForTest ( "n4" , "i4" )
// create three hosts
host1 , _ := createHostThenEnrollMDM ( s . ds , s . server . URL , t )
host2 , _ := createHostThenEnrollMDM ( s . ds , s . server . URL , t )
host3 , _ := createHostThenEnrollMDM ( s . ds , s . server . URL , t )
s . runWorker ( )
2024-09-10 22:44:58 +00:00
// Set up a mock Apple DEP API
s . enableABM ( t . Name ( ) )
s . mockDEPResponse ( t . Name ( ) , http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
encoder := json . NewEncoder ( w )
switch r . URL . Path {
case "/session" :
_ , _ = w . Write ( [ ] byte ( ` { "auth_session_token": "session123"} ` ) )
case "/account" :
_ , _ = w . Write ( [ ] byte ( fmt . Sprintf ( ` { "admin_id": "admin123", "org_name": "%s"} ` , "foo" ) ) )
case "/profile" :
w . WriteHeader ( http . StatusOK )
require . NoError ( t , encoder . Encode ( godep . ProfileResponse { ProfileUUID : "profile123" } ) )
}
} ) )
2024-04-18 21:01:37 +00:00
// Use a gitops user for all Puppet actions
u := & fleet . User {
Name : "GitOps" ,
Email : "gitops-TestPuppetRun@example.com" ,
GlobalRole : ptr . String ( fleet . RoleGitOps ) ,
}
require . NoError ( t , u . SetPassword ( test . GoodPassword , 10 , 10 ) )
_ , err := s . ds . NewUser ( context . Background ( ) , u )
require . NoError ( t , err )
s . setTokenForTest ( t , "gitops-TestPuppetRun@example.com" , test . GoodPassword )
// preassignAndMatch simulates the puppet module doing all the
// preassign/match calls for a given set of profiles.
preassignAndMatch := func ( profs [ ] fleet . MDMApplePreassignProfilePayload ) {
require . NotEmpty ( t , profs )
for _ , prof := range profs {
s . Do (
"POST" ,
"/api/latest/fleet/mdm/apple/profiles/preassign" ,
preassignMDMAppleProfileRequest { MDMApplePreassignProfilePayload : prof } ,
http . StatusNoContent ,
)
}
s . Do (
"POST" ,
"/api/latest/fleet/mdm/apple/profiles/match" ,
matchMDMApplePreassignmentRequest { ExternalHostIdentifier : profs [ 0 ] . ExternalHostIdentifier } ,
http . StatusNoContent ,
)
}
// node default {
// fleetdm::profile { 'n1':
// template => template('n1.mobileconfig.erb'),
// group => 'base',
// }
//
// fleetdm::profile { 'n2':
// template => template('n2.mobileconfig.erb'),
// group => 'workstations',
// }
//
// fleetdm::profile { 'n3':
// template => template('n3.mobileconfig.erb'),
// group => 'workstations',
// }
//
// if $facts['system_profiler']['hardware_uuid'] == 'host_2_uuid' {
// fleetdm::profile { 'n4':
// template => template('fleetdm/n4.mobileconfig.erb'),
// group => 'kiosks',
// }
// }
puppetRun := func ( host * fleet . Host ) {
payload := [ ] fleet . MDMApplePreassignProfilePayload {
{
ExternalHostIdentifier : host . Hostname ,
HostUUID : host . UUID ,
Profile : prof1 ,
Group : "base" ,
} ,
{
ExternalHostIdentifier : host . Hostname ,
HostUUID : host . UUID ,
Profile : prof2 ,
Group : "workstations" ,
} ,
{
ExternalHostIdentifier : host . Hostname ,
HostUUID : host . UUID ,
Profile : prof3 ,
Group : "workstations" ,
} ,
}
if host . UUID == host2 . UUID {
payload = append ( payload , fleet . MDMApplePreassignProfilePayload {
ExternalHostIdentifier : host . Hostname ,
HostUUID : host . UUID ,
Profile : prof4 ,
Group : "kiosks" ,
} )
}
preassignAndMatch ( payload )
}
// host1 checks in
puppetRun ( host1 )
// the host now belongs to a team
h1 , err := s . ds . Host ( ctx , host1 . ID )
require . NoError ( t , err )
require . NotNil ( t , h1 . TeamID )
// the team has the right name
2025-11-12 14:09:49 +00:00
tm1 , err := s . ds . TeamWithExtras ( ctx , * h1 . TeamID )
2024-04-18 21:01:37 +00:00
require . NoError ( t , err )
require . Equal ( t , "base - workstations" , tm1 . Name )
// and the right profiles
profs , err := s . ds . ListMDMAppleConfigProfiles ( ctx , & tm1 . ID )
require . NoError ( t , err )
require . Len ( t , profs , 3 )
require . Equal ( t , prof1 , [ ] byte ( profs [ 0 ] . Mobileconfig ) )
require . Equal ( t , prof2 , [ ] byte ( profs [ 1 ] . Mobileconfig ) )
require . Equal ( t , prof3 , [ ] byte ( profs [ 2 ] . Mobileconfig ) )
require . True ( t , tm1 . Config . MDM . EnableDiskEncryption )
// host2 checks in
puppetRun ( host2 )
// a new team is created
h2 , err := s . ds . Host ( ctx , host2 . ID )
require . NoError ( t , err )
require . NotNil ( t , h2 . TeamID )
// the team has the right name
2025-11-12 14:09:49 +00:00
tm2 , err := s . ds . TeamWithExtras ( ctx , * h2 . TeamID )
2024-04-18 21:01:37 +00:00
require . NoError ( t , err )
require . Equal ( t , "base - kiosks - workstations" , tm2 . Name )
// and the right profiles
profs , err = s . ds . ListMDMAppleConfigProfiles ( ctx , & tm2 . ID )
require . NoError ( t , err )
require . Len ( t , profs , 4 )
require . Equal ( t , prof1 , [ ] byte ( profs [ 0 ] . Mobileconfig ) )
require . Equal ( t , prof2 , [ ] byte ( profs [ 1 ] . Mobileconfig ) )
require . Equal ( t , prof3 , [ ] byte ( profs [ 2 ] . Mobileconfig ) )
require . Equal ( t , prof4 , [ ] byte ( profs [ 3 ] . Mobileconfig ) )
require . True ( t , tm2 . Config . MDM . EnableDiskEncryption )
// host3 checks in
puppetRun ( host3 )
// it belongs to the same team as host1
h3 , err := s . ds . Host ( ctx , host3 . ID )
require . NoError ( t , err )
require . Equal ( t , h1 . TeamID , h3 . TeamID )
// prof2 is edited
oldProf2 := prof2
prof2 = mobileconfigForTest ( "n2" , "i2-v2" )
// host3 checks in again
puppetRun ( host3 )
// still belongs to the same team
h3 , err = s . ds . Host ( ctx , host3 . ID )
require . NoError ( t , err )
require . Equal ( t , tm1 . ID , * h3 . TeamID )
// but the team has prof2 updated
profs , err = s . ds . ListMDMAppleConfigProfiles ( ctx , & tm1 . ID )
require . NoError ( t , err )
require . Len ( t , profs , 3 )
require . Equal ( t , prof1 , [ ] byte ( profs [ 0 ] . Mobileconfig ) )
require . Equal ( t , prof2 , [ ] byte ( profs [ 1 ] . Mobileconfig ) )
require . Equal ( t , prof3 , [ ] byte ( profs [ 2 ] . Mobileconfig ) )
require . NotEqual ( t , oldProf2 , [ ] byte ( profs [ 1 ] . Mobileconfig ) )
require . True ( t , tm1 . Config . MDM . EnableDiskEncryption )
// host2 checks in, still belongs to the same team
puppetRun ( host2 )
h2 , err = s . ds . Host ( ctx , host2 . ID )
require . NoError ( t , err )
require . Equal ( t , tm2 . ID , * h2 . TeamID )
// but the team has prof2 updated as well
profs , err = s . ds . ListMDMAppleConfigProfiles ( ctx , & tm2 . ID )
require . NoError ( t , err )
require . Len ( t , profs , 4 )
require . Equal ( t , prof1 , [ ] byte ( profs [ 0 ] . Mobileconfig ) )
require . Equal ( t , prof2 , [ ] byte ( profs [ 1 ] . Mobileconfig ) )
require . Equal ( t , prof3 , [ ] byte ( profs [ 2 ] . Mobileconfig ) )
require . Equal ( t , prof4 , [ ] byte ( profs [ 3 ] . Mobileconfig ) )
require . NotEqual ( t , oldProf2 , [ ] byte ( profs [ 1 ] . Mobileconfig ) )
require . True ( t , tm1 . Config . MDM . EnableDiskEncryption )
// the puppet manifest is changed, and prof3 is removed
// node default {
// fleetdm::profile { 'n1':
// template => template('n1.mobileconfig.erb'),
// group => 'base',
// }
//
// fleetdm::profile { 'n2':
// template => template('n2.mobileconfig.erb'),
// group => 'workstations',
// }
//
// if $facts['system_profiler']['hardware_uuid'] == 'host_2_uuid' {
// fleetdm::profile { 'n4':
// template => template('fleetdm/n4.mobileconfig.erb'),
// group => 'kiosks',
// }
// }
puppetRun = func ( host * fleet . Host ) {
payload := [ ] fleet . MDMApplePreassignProfilePayload {
{
ExternalHostIdentifier : host . Hostname ,
HostUUID : host . UUID ,
Profile : prof1 ,
Group : "base" ,
} ,
{
ExternalHostIdentifier : host . Hostname ,
HostUUID : host . UUID ,
Profile : prof2 ,
Group : "workstations" ,
} ,
}
if host . UUID == host2 . UUID {
payload = append ( payload , fleet . MDMApplePreassignProfilePayload {
ExternalHostIdentifier : host . Hostname ,
HostUUID : host . UUID ,
Profile : prof4 ,
Group : "kiosks" ,
} )
}
preassignAndMatch ( payload )
}
// host1 checks in again
puppetRun ( host1 )
// still belongs to the same team
h1 , err = s . ds . Host ( ctx , host1 . ID )
require . NoError ( t , err )
require . Equal ( t , tm1 . ID , * h1 . TeamID )
// but the team doesn't have prof3 anymore
profs , err = s . ds . ListMDMAppleConfigProfiles ( ctx , & tm1 . ID )
require . NoError ( t , err )
require . Len ( t , profs , 2 )
require . Equal ( t , prof1 , [ ] byte ( profs [ 0 ] . Mobileconfig ) )
require . Equal ( t , prof2 , [ ] byte ( profs [ 1 ] . Mobileconfig ) )
require . True ( t , tm1 . Config . MDM . EnableDiskEncryption )
// same for host2
puppetRun ( host2 )
h2 , err = s . ds . Host ( ctx , host2 . ID )
require . NoError ( t , err )
require . Equal ( t , tm2 . ID , * h2 . TeamID )
profs , err = s . ds . ListMDMAppleConfigProfiles ( ctx , & tm2 . ID )
require . NoError ( t , err )
require . Len ( t , profs , 3 )
require . Equal ( t , prof1 , [ ] byte ( profs [ 0 ] . Mobileconfig ) )
require . Equal ( t , prof2 , [ ] byte ( profs [ 1 ] . Mobileconfig ) )
require . Equal ( t , prof4 , [ ] byte ( profs [ 2 ] . Mobileconfig ) )
require . True ( t , tm1 . Config . MDM . EnableDiskEncryption )
// The puppet manifest is drastically updated, this time to use exclusions on host3:
//
// node default {
// fleetdm::profile { 'n1':
// template => template('n1.mobileconfig.erb'),
// group => 'base',
// }
//
// fleetdm::profile { 'n2':
// template => template('n2.mobileconfig.erb'),
// group => 'workstations',
// }
//
// if $facts['system_profiler']['hardware_uuid'] == 'host_3_uuid' {
// fleetdm::profile { 'n3':
// template => template('fleetdm/n3.mobileconfig.erb'),
// group => 'no-nudge',
// }
// } else {
// fleetdm::profile { 'n3':
// ensure => absent,
// template => template('fleetdm/n3.mobileconfig.erb'),
// group => 'workstations',
// }
// }
// }
puppetRun = func ( host * fleet . Host ) {
manifest := [ ] fleet . MDMApplePreassignProfilePayload {
{
ExternalHostIdentifier : host . Hostname ,
HostUUID : host . UUID ,
Profile : prof1 ,
Group : "base" ,
} ,
{
ExternalHostIdentifier : host . Hostname ,
HostUUID : host . UUID ,
Profile : prof2 ,
Group : "workstations" ,
} ,
}
if host . UUID == host3 . UUID {
manifest = append ( manifest , fleet . MDMApplePreassignProfilePayload {
ExternalHostIdentifier : host . Hostname ,
HostUUID : host . UUID ,
Profile : prof3 ,
Group : "no-nudge" ,
Exclude : true ,
} )
} else {
manifest = append ( manifest , fleet . MDMApplePreassignProfilePayload {
ExternalHostIdentifier : host . Hostname ,
HostUUID : host . UUID ,
Profile : prof3 ,
Group : "workstations" ,
} )
}
preassignAndMatch ( manifest )
}
// host1 checks in
puppetRun ( host1 )
// the host belongs to the same team
h1 , err = s . ds . Host ( ctx , host1 . ID )
require . NoError ( t , err )
require . Equal ( t , tm1 . ID , * h1 . TeamID )
// the team has the right profiles
profs , err = s . ds . ListMDMAppleConfigProfiles ( ctx , & tm1 . ID )
require . NoError ( t , err )
require . Len ( t , profs , 3 )
require . Equal ( t , prof1 , [ ] byte ( profs [ 0 ] . Mobileconfig ) )
require . Equal ( t , prof2 , [ ] byte ( profs [ 1 ] . Mobileconfig ) )
require . Equal ( t , prof3 , [ ] byte ( profs [ 2 ] . Mobileconfig ) )
require . True ( t , tm1 . Config . MDM . EnableDiskEncryption )
// host2 checks in
puppetRun ( host2 )
// it is assigned to tm1
h2 , err = s . ds . Host ( ctx , host2 . ID )
require . NoError ( t , err )
require . Equal ( t , tm1 . ID , * h2 . TeamID )
// host3 checks in
puppetRun ( host3 )
// it is assigned to a new team
h3 , err = s . ds . Host ( ctx , host3 . ID )
require . NoError ( t , err )
require . NotNil ( t , h3 . TeamID )
require . NotEqual ( t , tm1 . ID , * h3 . TeamID )
require . NotEqual ( t , tm2 . ID , * h3 . TeamID )
// a new team is created
2025-11-12 14:09:49 +00:00
tm3 , err := s . ds . TeamWithExtras ( ctx , * h3 . TeamID )
2024-04-18 21:01:37 +00:00
require . NoError ( t , err )
require . Equal ( t , "base - no-nudge - workstations" , tm3 . Name )
// and the right profiles
profs , err = s . ds . ListMDMAppleConfigProfiles ( ctx , & tm3 . ID )
require . NoError ( t , err )
require . Len ( t , profs , 2 )
require . Equal ( t , prof1 , [ ] byte ( profs [ 0 ] . Mobileconfig ) )
require . Equal ( t , prof2 , [ ] byte ( profs [ 1 ] . Mobileconfig ) )
require . True ( t , tm3 . Config . MDM . EnableDiskEncryption )
}
func ( s * integrationMDMTestSuite ) TestMDMAppleListConfigProfiles ( ) {
t := s . T ( )
ctx := context . Background ( )
testTeam , err := s . ds . NewTeam ( ctx , & fleet . Team { Name : "TestTeam" } )
require . NoError ( t , err )
mdmHost , _ := createHostThenEnrollMDM ( s . ds , s . server . URL , t )
s . runWorker ( )
t . Run ( "no profiles" , func ( t * testing . T ) {
var listResp listMDMAppleConfigProfilesResponse
s . DoJSON ( "GET" , "/api/v1/fleet/mdm/apple/profiles" , nil , http . StatusOK , & listResp )
require . NotNil ( t , listResp . ConfigProfiles ) // expect empty slice instead of nil
require . Len ( t , listResp . ConfigProfiles , 0 )
listResp = listMDMAppleConfigProfilesResponse { }
s . DoJSON ( "GET" , fmt . Sprintf ( ` /api/v1/fleet/mdm/apple/profiles?team_id=%d ` , testTeam . ID ) , nil , http . StatusOK , & listResp )
require . NotNil ( t , listResp . ConfigProfiles ) // expect empty slice instead of nil
require . Len ( t , listResp . ConfigProfiles , 0 )
var hostProfilesResp getHostProfilesResponse
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/v1/fleet/hosts/%d/configuration_profiles" , mdmHost . ID ) , nil , http . StatusOK , & hostProfilesResp )
require . NotNil ( t , hostProfilesResp . Profiles ) // expect empty slice instead of nil
require . Len ( t , hostProfilesResp . Profiles , 0 )
require . EqualValues ( t , mdmHost . ID , hostProfilesResp . HostID )
} )
t . Run ( "with profiles" , func ( t * testing . T ) {
p1 , err := fleet . NewMDMAppleConfigProfile ( mcBytesForTest ( "p1" , "p1.identifier" , "p1.uuid" ) , nil )
require . NoError ( t , err )
2025-04-30 20:03:23 +00:00
_ , err = s . ds . NewMDMAppleConfigProfile ( ctx , * p1 , nil )
2024-04-18 21:01:37 +00:00
require . NoError ( t , err )
p2 , err := fleet . NewMDMAppleConfigProfile ( mcBytesForTest ( "p2" , "p2.identifier" , "p2.uuid" ) , & testTeam . ID )
require . NoError ( t , err )
2025-04-30 20:03:23 +00:00
_ , err = s . ds . NewMDMAppleConfigProfile ( ctx , * p2 , nil )
2024-04-18 21:01:37 +00:00
require . NoError ( t , err )
var resp listMDMAppleConfigProfilesResponse
s . DoJSON ( "GET" , "/api/latest/fleet/mdm/apple/profiles" , listMDMAppleConfigProfilesRequest { TeamID : 0 } , http . StatusOK , & resp )
require . NotNil ( t , resp . ConfigProfiles )
require . Len ( t , resp . ConfigProfiles , 1 )
require . Equal ( t , p1 . Name , resp . ConfigProfiles [ 0 ] . Name )
require . Equal ( t , p1 . Identifier , resp . ConfigProfiles [ 0 ] . Identifier )
resp = listMDMAppleConfigProfilesResponse { }
s . DoJSON ( "GET" , fmt . Sprintf ( ` /api/v1/fleet/mdm/apple/profiles?team_id=%d ` , testTeam . ID ) , nil , http . StatusOK , & resp )
require . NotNil ( t , resp . ConfigProfiles )
require . Len ( t , resp . ConfigProfiles , 1 )
require . Equal ( t , p2 . Name , resp . ConfigProfiles [ 0 ] . Name )
require . Equal ( t , p2 . Identifier , resp . ConfigProfiles [ 0 ] . Identifier )
p3 , err := fleet . NewMDMAppleConfigProfile ( mcBytesForTest ( "p3" , "p3.identifier" , "p3.uuid" ) , & testTeam . ID )
require . NoError ( t , err )
2025-04-30 20:03:23 +00:00
_ , err = s . ds . NewMDMAppleConfigProfile ( ctx , * p3 , nil )
2024-04-18 21:01:37 +00:00
require . NoError ( t , err )
resp = listMDMAppleConfigProfilesResponse { }
s . DoJSON ( "GET" , fmt . Sprintf ( ` /api/v1/fleet/mdm/apple/profiles?team_id=%d ` , testTeam . ID ) , nil , http . StatusOK , & resp )
require . NotNil ( t , resp . ConfigProfiles )
require . Len ( t , resp . ConfigProfiles , 2 )
for _ , p := range resp . ConfigProfiles {
2024-10-18 17:38:26 +00:00
if p . Name == p2 . Name { //nolint:gocritic // ignore ifElseChain
2024-04-18 21:01:37 +00:00
require . Equal ( t , p2 . Identifier , p . Identifier )
} else if p . Name == p3 . Name {
require . Equal ( t , p3 . Identifier , p . Identifier )
} else {
require . Fail ( t , "unexpected profile name" )
}
}
var hostProfilesResp getHostProfilesResponse
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/v1/fleet/hosts/%d/configuration_profiles" , mdmHost . ID ) , nil , http . StatusOK , & hostProfilesResp )
require . NotNil ( t , hostProfilesResp . Profiles )
require . Len ( t , hostProfilesResp . Profiles , 1 )
require . Equal ( t , p1 . Name , hostProfilesResp . Profiles [ 0 ] . Name )
require . Equal ( t , p1 . Identifier , hostProfilesResp . Profiles [ 0 ] . Identifier )
require . EqualValues ( t , mdmHost . ID , hostProfilesResp . HostID )
// add the host to a team
2025-07-17 14:20:49 +00:00
err = s . ds . AddHostsToTeam ( ctx , fleet . NewAddHostsToTeamParams ( & testTeam . ID , [ ] uint { mdmHost . ID } ) )
2024-04-18 21:01:37 +00:00
require . NoError ( t , err )
hostProfilesResp = getHostProfilesResponse { }
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/v1/fleet/hosts/%d/configuration_profiles" , mdmHost . ID ) , nil , http . StatusOK , & hostProfilesResp )
require . NotNil ( t , hostProfilesResp . Profiles )
require . Len ( t , hostProfilesResp . Profiles , 2 )
require . EqualValues ( t , mdmHost . ID , hostProfilesResp . HostID )
} )
}
2024-06-25 19:26:28 +00:00
func ( s * integrationMDMTestSuite ) TestAppConfigMDMCustomSettings ( ) {
2024-04-18 21:01:37 +00:00
t := s . T ( )
2024-06-25 19:26:28 +00:00
// set the macos custom settings fields with the deprecated Labels field
2024-04-18 21:01:37 +00:00
acResp := appConfigResponse { }
s . DoJSON ( "PATCH" , "/api/latest/fleet/config" , json . RawMessage ( ` {
"mdm" : {
"macos_settings" : {
2024-06-25 19:26:28 +00:00
"custom_settings" : [
{ "path" : "foo" , "labels" : [ "baz" ] } ,
{ "path" : "bar" }
]
}
2024-04-18 21:01:37 +00:00
}
} ` ) , http . StatusOK , & acResp )
2024-06-25 19:26:28 +00:00
assert . Equal ( t , [ ] fleet . MDMProfileSpec { { Path : "foo" , LabelsIncludeAll : [ ] string { "baz" } } , { Path : "bar" } } , acResp . MDM . MacOSSettings . CustomSettings )
2024-04-18 21:01:37 +00:00
// check that they are returned by a GET /config
acResp = appConfigResponse { }
s . DoJSON ( "GET" , "/api/latest/fleet/config" , nil , http . StatusOK , & acResp )
2024-06-25 19:26:28 +00:00
assert . Equal ( t , [ ] fleet . MDMProfileSpec { { Path : "foo" , LabelsIncludeAll : [ ] string { "baz" } } , { Path : "bar" } } , acResp . MDM . MacOSSettings . CustomSettings )
// set the windows custom settings fields with included/excluded labels
acResp = appConfigResponse { }
s . DoJSON ( "PATCH" , "/api/latest/fleet/config" , json . RawMessage ( ` {
"mdm" : {
"windows_settings" : {
"custom_settings" : [
{ "path" : "foo" , "labels_exclude_any" : [ "x" , "y" ] } ,
{ "path" : "bar" , "labels_include_all" : [ "a" , "b" ] } ,
{ "path" : "baz" , "labels" : [ "c" ] }
]
}
}
} ` ) , http . StatusOK , & acResp )
assert . Equal ( t , [ ] fleet . MDMProfileSpec { { Path : "foo" , LabelsIncludeAll : [ ] string { "baz" } } , { Path : "bar" } } , acResp . MDM . MacOSSettings . CustomSettings )
assert . Equal ( t , optjson . SetSlice ( [ ] fleet . MDMProfileSpec { { Path : "foo" , LabelsExcludeAny : [ ] string { "x" , "y" } } , { Path : "bar" , LabelsIncludeAll : [ ] string { "a" , "b" } } , { Path : "baz" , LabelsIncludeAll : [ ] string { "c" } } } ) , acResp . MDM . WindowsSettings . CustomSettings )
2024-04-18 21:01:37 +00:00
2024-06-25 19:26:28 +00:00
// check that they are returned by a GET /config
acResp = appConfigResponse { }
s . DoJSON ( "GET" , "/api/latest/fleet/config" , nil , http . StatusOK , & acResp )
assert . Equal ( t , [ ] fleet . MDMProfileSpec { { Path : "foo" , LabelsIncludeAll : [ ] string { "baz" } } , { Path : "bar" } } , acResp . MDM . MacOSSettings . CustomSettings )
assert . Equal ( t , optjson . SetSlice ( [ ] fleet . MDMProfileSpec { { Path : "foo" , LabelsExcludeAny : [ ] string { "x" , "y" } } , { Path : "bar" , LabelsIncludeAll : [ ] string { "a" , "b" } } , { Path : "baz" , LabelsIncludeAll : [ ] string { "c" } } } ) , acResp . MDM . WindowsSettings . CustomSettings )
// patch without specifying the windows/macos custom settings fields and an unrelated
2024-04-18 21:01:37 +00:00
// field, should not remove them
acResp = appConfigResponse { }
s . DoJSON ( "PATCH" , "/api/latest/fleet/config" , json . RawMessage ( ` {
"mdm" : { "enable_disk_encryption" : true }
} ` ) , http . StatusOK , & acResp )
2024-06-25 19:26:28 +00:00
assert . Equal ( t , [ ] fleet . MDMProfileSpec { { Path : "foo" , LabelsIncludeAll : [ ] string { "baz" } } , { Path : "bar" } } , acResp . MDM . MacOSSettings . CustomSettings )
assert . Equal ( t , optjson . SetSlice ( [ ] fleet . MDMProfileSpec { { Path : "foo" , LabelsExcludeAny : [ ] string { "x" , "y" } } , { Path : "bar" , LabelsIncludeAll : [ ] string { "a" , "b" } } , { Path : "baz" , LabelsIncludeAll : [ ] string { "c" } } } ) , acResp . MDM . WindowsSettings . CustomSettings )
2024-04-18 21:01:37 +00:00
2024-06-25 19:26:28 +00:00
// patch with explicitly empty macos/windows custom settings fields, would remove
2024-04-18 21:01:37 +00:00
// them but this is a dry-run
acResp = appConfigResponse { }
s . DoJSON ( "PATCH" , "/api/latest/fleet/config" , json . RawMessage ( ` {
2024-06-25 19:26:28 +00:00
"mdm" : {
"macos_settings" : { "custom_settings" : null } ,
"windows_settings" : { "custom_settings" : null }
}
2024-04-18 21:01:37 +00:00
} ` ) , http . StatusOK , & acResp , "dry_run" , "true" )
2024-06-25 19:26:28 +00:00
assert . Equal ( t , [ ] fleet . MDMProfileSpec { { Path : "foo" , LabelsIncludeAll : [ ] string { "baz" } } , { Path : "bar" } } , acResp . MDM . MacOSSettings . CustomSettings )
assert . Equal ( t , optjson . SetSlice ( [ ] fleet . MDMProfileSpec { { Path : "foo" , LabelsExcludeAny : [ ] string { "x" , "y" } } , { Path : "bar" , LabelsIncludeAll : [ ] string { "a" , "b" } } , { Path : "baz" , LabelsIncludeAll : [ ] string { "c" } } } ) , acResp . MDM . WindowsSettings . CustomSettings )
2024-04-18 21:01:37 +00:00
// patch with explicitly empty macos custom settings fields, removes them
acResp = appConfigResponse { }
s . DoJSON ( "PATCH" , "/api/latest/fleet/config" , json . RawMessage ( ` {
2024-06-25 19:26:28 +00:00
"mdm" : {
"macos_settings" : { "custom_settings" : null } ,
"windows_settings" : { "custom_settings" : null }
}
2024-04-18 21:01:37 +00:00
} ` ) , http . StatusOK , & acResp )
assert . Empty ( t , acResp . MDM . MacOSSettings . CustomSettings )
2024-06-25 19:26:28 +00:00
assert . Equal ( t , optjson . Slice [ fleet . MDMProfileSpec ] { Set : true , Value : [ ] fleet . MDMProfileSpec { } } , acResp . MDM . WindowsSettings . CustomSettings )
// mix of labels fields returns an error
res := s . Do ( "PATCH" , "/api/latest/fleet/config" , json . RawMessage ( ` {
"mdm" : {
"macos_settings" : {
"custom_settings" : [
{ "path" : "foo" , "labels" : [ "a" ] , "labels_exclude_any" : [ "b" ] }
]
}
}
} ` ) , http . StatusUnprocessableEntity )
msg := extractServerErrorText ( res . Body )
2024-11-05 20:13:44 +00:00
require . Contains ( t , msg , ` For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included. ` )
2024-06-25 19:26:28 +00:00
res = s . Do ( "PATCH" , "/api/latest/fleet/config" , json . RawMessage ( ` {
"mdm" : {
"windows_settings" : {
"custom_settings" : [
{ "path" : "foo" , "labels_include_all" : [ "a" ] , "labels_exclude_any" : [ "b" ] }
]
}
}
} ` ) , http . StatusUnprocessableEntity )
msg = extractServerErrorText ( res . Body )
2024-11-05 20:13:44 +00:00
require . Contains ( t , msg , ` For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included. ` )
res = s . Do ( "PATCH" , "/api/latest/fleet/config" , json . RawMessage ( ` {
"mdm" : {
"windows_settings" : {
"custom_settings" : [
{ "path" : "foo" , "labels_include_any" : [ "a" ] , "labels_exclude_any" : [ "b" ] }
]
}
}
} ` ) , 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. ` )
res = s . Do ( "PATCH" , "/api/latest/fleet/config" , json . RawMessage ( ` {
"mdm" : {
"windows_settings" : {
"custom_settings" : [
{ "path" : "foo" , "labels" : [ "a" ] , "labels_include_any" : [ "b" ] }
]
}
}
} ` ) , 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. ` )
2024-04-18 21:01:37 +00:00
}
func ( s * integrationMDMTestSuite ) TestApplyTeamsMDMAppleProfiles ( ) {
t := s . T ( )
// create a team through the service so it initializes the agent ops
teamName := t . Name ( ) + "team1"
team := & fleet . Team {
Name : teamName ,
Description : "desc team1" ,
}
var createTeamResp teamResponse
s . DoJSON ( "POST" , "/api/latest/fleet/teams" , team , http . StatusOK , & createTeamResp )
require . NotZero ( t , createTeamResp . Team . ID )
team = createTeamResp . Team
// apply with custom macos settings
teamSpecs := applyTeamSpecsRequest { Specs : [ ] * fleet . TeamSpec { {
Name : teamName ,
MDM : fleet . TeamSpecMDM {
MacOSSettings : map [ string ] interface { } {
2024-06-25 19:26:28 +00:00
"custom_settings" : [ ] map [ string ] interface { } {
{ "path" : "foo" , "labels" : [ ] string { "a" , "b" } } ,
{ "path" : "bar" , "labels_exclude_any" : [ ] string { "c" } } ,
} ,
2024-04-18 21:01:37 +00:00
} ,
} ,
} } }
s . Do ( "POST" , "/api/latest/fleet/spec/teams" , teamSpecs , http . StatusOK )
// retrieving the team returns the custom macos settings
var teamResp getTeamResponse
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/latest/fleet/teams/%d" , team . ID ) , nil , http . StatusOK , & teamResp )
2024-06-25 19:26:28 +00:00
require . Equal ( t , [ ] fleet . MDMProfileSpec {
{ Path : "foo" , LabelsIncludeAll : [ ] string { "a" , "b" } } ,
{ Path : "bar" , LabelsExcludeAny : [ ] string { "c" } } ,
} , teamResp . Team . Config . MDM . MacOSSettings . CustomSettings )
2024-04-18 21:01:37 +00:00
// apply with invalid macos settings subfield should fail
teamSpecs = applyTeamSpecsRequest { Specs : [ ] * fleet . TeamSpec { {
Name : teamName ,
MDM : fleet . TeamSpecMDM {
MacOSSettings : map [ string ] interface { } { "foo_bar" : 123 } ,
} ,
} } }
res := s . Do ( "POST" , "/api/latest/fleet/spec/teams" , teamSpecs , http . StatusBadRequest )
errMsg := extractServerErrorText ( res . Body )
assert . Contains ( t , errMsg , ` unsupported key provided: "foo_bar" ` )
// apply with some good and some bad macos settings subfield should fail
teamSpecs = applyTeamSpecsRequest { Specs : [ ] * fleet . TeamSpec { {
Name : teamName ,
MDM : fleet . TeamSpecMDM {
MacOSSettings : map [ string ] interface { } { "custom_settings" : [ ] interface { } { "A" , true } } ,
} ,
} } }
res = s . Do ( "POST" , "/api/latest/fleet/spec/teams" , teamSpecs , http . StatusBadRequest )
errMsg = extractServerErrorText ( res . Body )
assert . Contains ( t , errMsg , ` invalid value type at 'macos_settings.custom_settings': expected array of MDMProfileSpecs but got bool ` )
// apply without custom macos settings specified and unrelated field, should
// not replace existing settings
teamSpecs = applyTeamSpecsRequest { Specs : [ ] * fleet . TeamSpec { {
Name : teamName ,
MDM : fleet . TeamSpecMDM {
EnableDiskEncryption : optjson . SetBool ( false ) ,
} ,
} } }
s . Do ( "POST" , "/api/latest/fleet/spec/teams" , teamSpecs , http . StatusOK )
teamResp = getTeamResponse { }
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/latest/fleet/teams/%d" , team . ID ) , nil , http . StatusOK , & teamResp )
2024-06-25 19:26:28 +00:00
require . Equal ( t , [ ] fleet . MDMProfileSpec {
{ Path : "foo" , LabelsIncludeAll : [ ] string { "a" , "b" } } ,
{ Path : "bar" , LabelsExcludeAny : [ ] string { "c" } } ,
} , teamResp . Team . Config . MDM . MacOSSettings . CustomSettings )
2024-04-18 21:01:37 +00:00
// apply with explicitly empty custom macos settings would clear the existing
// settings, but dry-run
teamSpecs = applyTeamSpecsRequest { Specs : [ ] * fleet . TeamSpec { {
Name : teamName ,
MDM : fleet . TeamSpecMDM {
MacOSSettings : map [ string ] interface { } { "custom_settings" : [ ] map [ string ] interface { } { } } ,
} ,
} } }
s . Do ( "POST" , "/api/latest/fleet/spec/teams" , teamSpecs , http . StatusOK , "dry_run" , "true" )
teamResp = getTeamResponse { }
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/latest/fleet/teams/%d" , team . ID ) , nil , http . StatusOK , & teamResp )
2024-06-25 19:26:28 +00:00
require . Equal ( t , [ ] fleet . MDMProfileSpec {
{ Path : "foo" , LabelsIncludeAll : [ ] string { "a" , "b" } } ,
{ Path : "bar" , LabelsExcludeAny : [ ] string { "c" } } ,
} , teamResp . Team . Config . MDM . MacOSSettings . CustomSettings )
2024-04-18 21:01:37 +00:00
// apply with explicitly empty custom macos settings clears the existing settings
teamSpecs = applyTeamSpecsRequest { Specs : [ ] * fleet . TeamSpec { {
Name : teamName ,
MDM : fleet . TeamSpecMDM {
MacOSSettings : map [ string ] interface { } { "custom_settings" : [ ] map [ string ] interface { } { } } ,
} ,
} } }
s . Do ( "POST" , "/api/latest/fleet/spec/teams" , teamSpecs , http . StatusOK )
teamResp = getTeamResponse { }
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/latest/fleet/teams/%d" , team . ID ) , nil , http . StatusOK , & teamResp )
require . Equal ( t , [ ] fleet . MDMProfileSpec { } , teamResp . Team . Config . MDM . MacOSSettings . CustomSettings )
2024-06-25 19:26:28 +00:00
// apply with invalid mix of labels fails
teamSpecs = applyTeamSpecsRequest { Specs : [ ] * fleet . TeamSpec { {
Name : teamName ,
MDM : fleet . TeamSpecMDM {
MacOSSettings : map [ string ] interface { } {
"custom_settings" : [ ] map [ string ] interface { } {
{ "path" : "bar" , "labels" : [ ] string { "x" } } ,
{ "path" : "foo" , "labels" : [ ] string { "a" , "b" } , "labels_include_all" : [ ] string { "c" } } ,
} ,
} ,
} ,
} } }
res = s . Do ( "POST" , "/api/latest/fleet/spec/teams" , teamSpecs , http . StatusUnprocessableEntity )
errMsg = extractServerErrorText ( res . Body )
2024-11-05 20:13:44 +00:00
assert . Contains ( t , errMsg , ` For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included. ` )
2024-04-18 21:01:37 +00:00
}
func ( s * integrationMDMTestSuite ) TestBatchSetMDMAppleProfiles ( ) {
t := s . T ( )
ctx := context . Background ( )
2024-06-18 17:02:00 +00:00
bigString := strings . Repeat ( "a" , 1024 * 1024 + 1 )
2024-04-18 21:01:37 +00:00
// create a new team
tm , err := s . ds . NewTeam ( ctx , & fleet . Team { Name : "batch_set_mdm_profiles" } )
require . NoError ( t , err )
// apply an empty set to no-team
s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" , batchSetMDMAppleProfilesRequest { Profiles : nil } , http . StatusNoContent )
s . lastActivityMatches (
fleet . ActivityTypeEditedMacosProfile { } . ActivityName ( ) ,
` { "team_id": null, "team_name": null} ` ,
0 ,
)
// apply to both team id and name
s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" , batchSetMDMAppleProfilesRequest { Profiles : nil } ,
2024-10-18 17:38:26 +00:00
http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) , "team_name" , tm . Name )
2024-04-18 21:01:37 +00:00
// invalid team name
s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" , batchSetMDMAppleProfilesRequest { Profiles : nil } ,
http . StatusNotFound , "team_name" , uuid . New ( ) . String ( ) )
2024-06-18 17:02:00 +00:00
// Profile is too big
resp := s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" , batchSetMDMAppleProfilesRequest { Profiles : [ ] [ ] byte { [ ] byte ( bigString ) } } ,
http . StatusUnprocessableEntity )
require . Contains ( t , extractServerErrorText ( resp . Body ) , "maximum configuration profile file size is 1 MB" )
2024-04-18 21:01:37 +00:00
// duplicate profile names
s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" , batchSetMDMAppleProfilesRequest { Profiles : [ ] [ ] byte {
mobileconfigForTest ( "N1" , "I1" ) ,
mobileconfigForTest ( "N1" , "I2" ) ,
2024-10-18 17:38:26 +00:00
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
2024-04-18 21:01:37 +00:00
// profiles with reserved identifiers
for p := range mobileconfig . FleetPayloadIdentifiers ( ) {
res := s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" , batchSetMDMAppleProfilesRequest { Profiles : [ ] [ ] byte {
mobileconfigForTest ( "N1" , "I1" ) ,
mobileconfigForTest ( p , p ) ,
2024-10-18 17:38:26 +00:00
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
2024-04-18 21:01:37 +00:00
errMsg := extractServerErrorText ( res . Body )
2025-03-07 14:33:40 +00:00
require . Contains ( t , errMsg , fmt . Sprintf ( "Validation Failed: payload identifier %s is not allowed" , p ) )
2024-04-18 21:01:37 +00:00
}
// payloads with reserved types
for p := range mobileconfig . FleetPayloadTypes ( ) {
2025-03-25 00:36:36 +00:00
if p == mobileconfig . FleetCustomSettingsPayloadType {
2025-03-25 14:08:47 +00:00
// FileVault options in the custom settings payload are checked in file_vault_options_test.go
2025-03-25 00:36:36 +00:00
continue
}
2024-04-18 21:01:37 +00:00
res := s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" , batchSetMDMAppleProfilesRequest { Profiles : [ ] [ ] byte {
mobileconfigForTestWithContent ( "N1" , "I1" , "II1" , p , "" ) ,
2024-10-18 17:38:26 +00:00
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
2024-04-18 21:01:37 +00:00
errMsg := extractServerErrorText ( res . Body )
2025-03-21 14:56:50 +00:00
switch p {
2025-03-25 00:36:36 +00:00
case mobileconfig . FleetFileVaultPayloadType , mobileconfig . FleetRecoveryKeyEscrowPayloadType :
2025-03-21 19:24:52 +00:00
assert . Contains ( t , errMsg , mobileconfig . DiskEncryptionProfileRestrictionErrMsg )
2025-03-21 14:56:50 +00:00
default :
assert . Contains ( t , errMsg , fmt . Sprintf ( "Validation Failed: unsupported PayloadType(s): %s" , p ) )
}
2024-04-18 21:01:37 +00:00
}
// payloads with reserved identifiers
for p := range mobileconfig . FleetPayloadIdentifiers ( ) {
res := s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" , batchSetMDMAppleProfilesRequest { Profiles : [ ] [ ] byte {
mobileconfigForTestWithContent ( "N1" , "I1" , p , "random" , "" ) ,
2024-10-18 17:38:26 +00:00
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
2024-04-18 21:01:37 +00:00
errMsg := extractServerErrorText ( res . Body )
2025-03-07 14:33:40 +00:00
require . Contains ( t , errMsg , fmt . Sprintf ( "Validation Failed: unsupported PayloadIdentifier(s): %s" , p ) )
2024-04-18 21:01:37 +00:00
}
// successfully apply a profile for the team
s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" , batchSetMDMAppleProfilesRequest { Profiles : [ ] [ ] byte {
mobileconfigForTest ( "N1" , "I1" ) ,
2024-10-18 17:38:26 +00:00
} } , http . StatusNoContent , "team_id" , fmt . Sprint ( tm . ID ) )
2024-04-18 21:01:37 +00:00
s . lastActivityMatches (
fleet . ActivityTypeEditedMacosProfile { } . ActivityName ( ) ,
fmt . Sprintf ( ` { "team_id": %d, "team_name": %q} ` , tm . ID , tm . Name ) ,
0 ,
)
}
func ( s * integrationMDMTestSuite ) TestHostMDMAppleProfilesStatus ( ) {
t := s . T ( )
ctx := context . Background ( )
2025-06-18 17:59:12 +00:00
createManualMDMEnrollWithOrbit := func ( secret string , doUserEnroll bool ) ( * fleet . Host , * fleet . NanoEnrollment , * mdmtest . TestAppleMDMClient ) {
2024-04-18 21:01:37 +00:00
// orbit enrollment happens before mdm enrollment, otherwise the host would
// always receive the "no team" profiles on mdm enrollment since it would
// not be part of any team yet (team assignment is done when it enrolls
// with orbit).
mdmDevice := mdmtest . NewTestMDMClientAppleDirect ( mdmtest . AppleEnrollInfo {
2024-06-03 21:33:52 +00:00
SCEPChallenge : s . scepChallenge ,
2024-04-18 21:01:37 +00:00
SCEPURL : s . server . URL + apple_mdm . SCEPPath ,
MDMURL : s . server . URL + apple_mdm . MDMPath ,
2024-06-10 20:02:35 +00:00
} , "MacBookPro16,1" )
2024-04-18 21:01:37 +00:00
// enroll the device with orbit
var resp EnrollOrbitResponse
2025-07-16 18:08:27 +00:00
s . DoJSON ( "POST" , "/api/fleet/orbit/enroll" , contract . EnrollOrbitRequest {
2024-04-18 21:01:37 +00:00
EnrollSecret : secret ,
HardwareUUID : mdmDevice . UUID , // will not match any existing host
HardwareSerial : mdmDevice . SerialNumber ,
} , http . StatusOK , & resp )
require . NotEmpty ( t , resp . OrbitNodeKey )
orbitNodeKey := resp . OrbitNodeKey
h , err := s . ds . LoadHostByOrbitNodeKey ( ctx , orbitNodeKey )
require . NoError ( t , err )
h . OrbitNodeKey = & orbitNodeKey
h . Platform = "darwin"
err = mdmDevice . Enroll ( )
require . NoError ( t , err )
2025-06-16 20:46:38 +00:00
var userEnrollment * fleet . NanoEnrollment
if doUserEnroll {
// Do a user enrollment with a bit of extra sanity checking
userEnrollment , err = s . ds . GetNanoMDMUserEnrollment ( ctx , h . UUID )
require . NoError ( t , err )
require . Nil ( t , userEnrollment )
err = mdmDevice . UserEnroll ( )
require . NoError ( t , err )
userEnrollment , err = s . ds . GetNanoMDMUserEnrollment ( ctx , h . UUID )
require . NoError ( t , err )
require . NotNil ( t , userEnrollment )
}
2025-06-18 17:59:12 +00:00
return h , userEnrollment , mdmDevice
2024-04-18 21:01:37 +00:00
}
2025-06-18 17:59:12 +00:00
triggerReconcileProfilesMarkVerifying := func ( ) {
Allow configuring webhook policy automations for "No team" (#32129)
Fixes #32060
This PR adds:
- new default_team_config_json table
- caching of config from that table, including deep copy methods -- all
of this is not absolutely needed for this change since we are only using
`webhook_settings.failing_policies_webhook` here but added for
completeness/future
- teams/0 API updates
- GitOps updates
- generate gitops updates
Future PRs will add:
- ticket automation
- primo mode migration
- frontend changes
- documentation
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked table schema to confirm autoupdate
## New Fleet configuration settings
- [x] Verified that the setting is exported via `fleetctl
generate-gitops`
- [x] Verified that the setting is cleared on the server if it is not
supplied in a YAML file (or that it is documented as being optional)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- New Features
- Configure failing-policy webhooks for “No team” via GitOps
(no-team.yml) and API, including enable/disable, destination URL, policy
IDs, and batch size; settings clear when omitted.
- GitOps and CLI now read/apply the real “No team” settings with dry-run
support.
- Policy automation evaluates hosts without a team and triggers “No
team” webhooks when applicable.
- GET/PATCH team 0 returns/accepts a minimal, webhook-focused config.
- Chores
- Added persistence and caching for the default “No team” configuration.
- Introduced a database table to store the default configuration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-08-28 21:38:27 +00:00
t . Logf ( "[TestHostMDMAppleProfilesStatus] Calling awaitTriggerProfileSchedule at %s" , time . Now ( ) . Format ( time . RFC3339 ) )
2024-04-18 21:01:37 +00:00
s . awaitTriggerProfileSchedule ( t )
Allow configuring webhook policy automations for "No team" (#32129)
Fixes #32060
This PR adds:
- new default_team_config_json table
- caching of config from that table, including deep copy methods -- all
of this is not absolutely needed for this change since we are only using
`webhook_settings.failing_policies_webhook` here but added for
completeness/future
- teams/0 API updates
- GitOps updates
- generate gitops updates
Future PRs will add:
- ticket automation
- primo mode migration
- frontend changes
- documentation
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked table schema to confirm autoupdate
## New Fleet configuration settings
- [x] Verified that the setting is exported via `fleetctl
generate-gitops`
- [x] Verified that the setting is cleared on the server if it is not
supplied in a YAML file (or that it is documented as being optional)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- New Features
- Configure failing-policy webhooks for “No team” via GitOps
(no-team.yml) and API, including enable/disable, destination URL, policy
IDs, and batch size; settings clear when omitted.
- GitOps and CLI now read/apply the real “No team” settings with dry-run
support.
- Policy automation evaluates hosts without a team and triggers “No
team” webhooks when applicable.
- GET/PATCH team 0 returns/accepts a minimal, webhook-focused config.
- Chores
- Added persistence and caching for the default “No team” configuration.
- Introduced a database table to store the default configuration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-08-28 21:38:27 +00:00
t . Logf ( "[TestHostMDMAppleProfilesStatus] awaitTriggerProfileSchedule completed, updating profiles to verifying at %s" , time . Now ( ) . Format ( time . RFC3339 ) )
2024-04-18 21:01:37 +00:00
// this will only mark them as "pending", as the response to confirm
// profile deployment is asynchronous, so we simulate it here by
// updating any "pending" (not NULL) profiles to "verifying"
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
_ , err := q . ExecContext ( ctx , ` UPDATE host_mdm_apple_profiles SET status = ? WHERE status = ? ` , fleet . OSSettingsVerifying , fleet . OSSettingsPending )
return err
} )
Allow configuring webhook policy automations for "No team" (#32129)
Fixes #32060
This PR adds:
- new default_team_config_json table
- caching of config from that table, including deep copy methods -- all
of this is not absolutely needed for this change since we are only using
`webhook_settings.failing_policies_webhook` here but added for
completeness/future
- teams/0 API updates
- GitOps updates
- generate gitops updates
Future PRs will add:
- ticket automation
- primo mode migration
- frontend changes
- documentation
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked table schema to confirm autoupdate
## New Fleet configuration settings
- [x] Verified that the setting is exported via `fleetctl
generate-gitops`
- [x] Verified that the setting is cleared on the server if it is not
supplied in a YAML file (or that it is documented as being optional)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- New Features
- Configure failing-policy webhooks for “No team” via GitOps
(no-team.yml) and API, including enable/disable, destination URL, policy
IDs, and batch size; settings clear when omitted.
- GitOps and CLI now read/apply the real “No team” settings with dry-run
support.
- Policy automation evaluates hosts without a team and triggers “No
team” webhooks when applicable.
- GET/PATCH team 0 returns/accepts a minimal, webhook-focused config.
- Chores
- Added persistence and caching for the default “No team” configuration.
- Introduced a database table to store the default configuration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-08-28 21:38:27 +00:00
t . Logf ( "[TestHostMDMAppleProfilesStatus] Profiles updated to verifying status at %s" , time . Now ( ) . Format ( time . RFC3339 ) )
2024-04-18 21:01:37 +00:00
}
assignHostToTeam := func ( h * fleet . Host , teamID * uint ) {
var moveHostResp addHostsToTeamResponse
s . DoJSON ( "POST" , "/api/v1/fleet/hosts/transfer" ,
addHostsToTeamRequest { TeamID : teamID , HostIDs : [ ] uint { h . ID } } , http . StatusOK , & moveHostResp )
h . TeamID = teamID
}
// add a couple global profiles
2025-06-16 20:46:38 +00:00
payloadScopeSystem := fleet . PayloadScopeSystem
payloadScopeUser := fleet . PayloadScopeUser
2024-04-18 21:01:37 +00:00
globalProfiles := [ ] [ ] byte {
mobileconfigForTest ( "G1" , "G1" ) ,
2025-06-16 20:46:38 +00:00
scopedMobileconfigForTest ( "G2" , "G2" , & payloadScopeSystem ) ,
2025-07-02 14:54:54 +00:00
scopedMobileconfigForTest ( "G3" , "G3.user" , & payloadScopeUser ) ,
2024-04-18 21:01:37 +00:00
}
s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" ,
batchSetMDMAppleProfilesRequest { Profiles : globalProfiles } , http . StatusNoContent )
// create the no-team enroll secret
var applyResp applyEnrollSecretSpecResponse
globalEnrollSec := "global_enroll_sec"
s . DoJSON ( "POST" , "/api/latest/fleet/spec/enroll_secret" ,
applyEnrollSecretSpecRequest {
Spec : & fleet . EnrollSecretSpec {
Secrets : [ ] * fleet . EnrollSecret { { Secret : globalEnrollSec } } ,
} ,
} , http . StatusOK , & applyResp )
// create a team with a couple profiles
tm1 , err := s . ds . NewTeam ( ctx , & fleet . Team { Name : "team_profiles_status_1" } )
require . NoError ( t , err )
tm1Profiles := [ ] [ ] byte {
mobileconfigForTest ( "T1.1" , "T1.1" ) ,
2025-06-16 20:46:38 +00:00
scopedMobileconfigForTest ( "T1.2" , "T1.2" , & payloadScopeSystem ) ,
2025-07-02 14:54:54 +00:00
scopedMobileconfigForTest ( "T1.3" , "T1.3.user" , & payloadScopeUser ) ,
2024-04-18 21:01:37 +00:00
}
s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" ,
batchSetMDMAppleProfilesRequest { Profiles : tm1Profiles } , http . StatusNoContent ,
2024-10-18 17:38:26 +00:00
"team_id" , fmt . Sprint ( tm1 . ID ) )
2024-04-18 21:01:37 +00:00
// create the team 1 enroll secret
var teamResp teamEnrollSecretsResponse
tm1EnrollSec := "team1_enroll_sec"
s . DoJSON ( "PATCH" , fmt . Sprintf ( "/api/latest/fleet/teams/%d/secrets" , tm1 . ID ) ,
modifyTeamEnrollSecretsRequest {
Secrets : [ ] fleet . EnrollSecret { { Secret : tm1EnrollSec } } ,
} , http . StatusOK , & teamResp )
// create another team with different profiles
tm2 , err := s . ds . NewTeam ( ctx , & fleet . Team { Name : "team_profiles_status_2" } )
require . NoError ( t , err )
tm2Profiles := [ ] [ ] byte {
mobileconfigForTest ( "T2.1" , "T2.1" ) ,
}
s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" ,
batchSetMDMAppleProfilesRequest { Profiles : tm2Profiles } , http . StatusNoContent ,
2024-10-18 17:38:26 +00:00
"team_id" , fmt . Sprint ( tm2 . ID ) )
2024-04-18 21:01:37 +00:00
// enroll a couple hosts in no team
2025-06-18 17:59:12 +00:00
h1 , h1UserEnrollment , _ := createManualMDMEnrollWithOrbit ( globalEnrollSec , true )
2024-04-18 21:01:37 +00:00
require . Nil ( t , h1 . TeamID )
2025-06-18 17:59:12 +00:00
h2 , _ , _ := createManualMDMEnrollWithOrbit ( globalEnrollSec , false )
2024-04-18 21:01:37 +00:00
require . Nil ( t , h2 . TeamID )
2024-04-29 19:43:15 +00:00
// run the cron
Allow configuring webhook policy automations for "No team" (#32129)
Fixes #32060
This PR adds:
- new default_team_config_json table
- caching of config from that table, including deep copy methods -- all
of this is not absolutely needed for this change since we are only using
`webhook_settings.failing_policies_webhook` here but added for
completeness/future
- teams/0 API updates
- GitOps updates
- generate gitops updates
Future PRs will add:
- ticket automation
- primo mode migration
- frontend changes
- documentation
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked table schema to confirm autoupdate
## New Fleet configuration settings
- [x] Verified that the setting is exported via `fleetctl
generate-gitops`
- [x] Verified that the setting is cleared on the server if it is not
supplied in a YAML file (or that it is documented as being optional)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- New Features
- Configure failing-policy webhooks for “No team” via GitOps
(no-team.yml) and API, including enable/disable, destination URL, policy
IDs, and batch size; settings clear when omitted.
- GitOps and CLI now read/apply the real “No team” settings with dry-run
support.
- Policy automation evaluates hosts without a team and triggers “No
team” webhooks when applicable.
- GET/PATCH team 0 returns/accepts a minimal, webhook-focused config.
- Chores
- Added persistence and caching for the default “No team” configuration.
- Introduced a database table to store the default configuration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-08-28 21:38:27 +00:00
t . Logf ( "[TestHostMDMAppleProfilesStatus] Starting FIRST cron run (after h1, h2 enrolled) at %s" , time . Now ( ) . Format ( time . RFC3339 ) )
2024-04-29 19:43:15 +00:00
s . awaitTriggerProfileSchedule ( t )
Allow configuring webhook policy automations for "No team" (#32129)
Fixes #32060
This PR adds:
- new default_team_config_json table
- caching of config from that table, including deep copy methods -- all
of this is not absolutely needed for this change since we are only using
`webhook_settings.failing_policies_webhook` here but added for
completeness/future
- teams/0 API updates
- GitOps updates
- generate gitops updates
Future PRs will add:
- ticket automation
- primo mode migration
- frontend changes
- documentation
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked table schema to confirm autoupdate
## New Fleet configuration settings
- [x] Verified that the setting is exported via `fleetctl
generate-gitops`
- [x] Verified that the setting is cleared on the server if it is not
supplied in a YAML file (or that it is documented as being optional)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- New Features
- Configure failing-policy webhooks for “No team” via GitOps
(no-team.yml) and API, including enable/disable, destination URL, policy
IDs, and batch size; settings clear when omitted.
- GitOps and CLI now read/apply the real “No team” settings with dry-run
support.
- Policy automation evaluates hosts without a team and triggers “No
team” webhooks when applicable.
- GET/PATCH team 0 returns/accepts a minimal, webhook-focused config.
- Chores
- Added persistence and caching for the default “No team” configuration.
- Introduced a database table to store the default configuration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-08-28 21:38:27 +00:00
t . Logf ( "[TestHostMDMAppleProfilesStatus] FIRST cron run completed at %s" , time . Now ( ) . Format ( time . RFC3339 ) )
2025-06-18 17:59:12 +00:00
// G3 is user-scoped and the h2 host doesn't have a user-channel yet (and
2025-07-02 14:54:54 +00:00
// enrolled just now, so the minimum delay to give up and fail the profile
// delivery is not reached)
2024-07-03 14:20:33 +00:00
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
2024-04-18 21:01:37 +00:00
h1 : {
{ Identifier : "G1" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "G2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
2025-07-02 14:54:54 +00:00
{ Identifier : "G3.user" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
2024-04-29 19:43:15 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
2024-04-18 21:01:37 +00:00
} ,
h2 : {
{ Identifier : "G1" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "G2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
2025-07-02 14:54:54 +00:00
{ Identifier : "G3.user" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
2024-04-29 19:43:15 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
2024-04-18 21:01:37 +00:00
} ,
} )
2025-06-16 20:46:38 +00:00
// Verify there is a command on the user channel for h1
enrollmentIds , err := s . ds . GetEnrollmentIDsWithPendingMDMAppleCommands ( ctx )
require . NoError ( t , err )
assert . Contains ( t , enrollmentIds , h1UserEnrollment . ID )
assert . Contains ( t , enrollmentIds , h1 . UUID )
assert . Contains ( t , enrollmentIds , h2 . UUID )
2024-04-18 21:01:37 +00:00
// enroll a couple hosts in team 1
2025-06-18 17:59:12 +00:00
h3 , h3UserEnrollment , _ := createManualMDMEnrollWithOrbit ( tm1EnrollSec , true )
2024-04-18 21:01:37 +00:00
require . NotNil ( t , h3 . TeamID )
require . Equal ( t , tm1 . ID , * h3 . TeamID )
2025-06-18 17:59:12 +00:00
h4 , _ , h4Device := createManualMDMEnrollWithOrbit ( tm1EnrollSec , false )
2024-04-18 21:01:37 +00:00
require . NotNil ( t , h4 . TeamID )
require . Equal ( t , tm1 . ID , * h4 . TeamID )
2025-06-16 20:46:38 +00:00
2024-04-29 19:43:15 +00:00
// run the cron
Allow configuring webhook policy automations for "No team" (#32129)
Fixes #32060
This PR adds:
- new default_team_config_json table
- caching of config from that table, including deep copy methods -- all
of this is not absolutely needed for this change since we are only using
`webhook_settings.failing_policies_webhook` here but added for
completeness/future
- teams/0 API updates
- GitOps updates
- generate gitops updates
Future PRs will add:
- ticket automation
- primo mode migration
- frontend changes
- documentation
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked table schema to confirm autoupdate
## New Fleet configuration settings
- [x] Verified that the setting is exported via `fleetctl
generate-gitops`
- [x] Verified that the setting is cleared on the server if it is not
supplied in a YAML file (or that it is documented as being optional)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- New Features
- Configure failing-policy webhooks for “No team” via GitOps
(no-team.yml) and API, including enable/disable, destination URL, policy
IDs, and batch size; settings clear when omitted.
- GitOps and CLI now read/apply the real “No team” settings with dry-run
support.
- Policy automation evaluates hosts without a team and triggers “No
team” webhooks when applicable.
- GET/PATCH team 0 returns/accepts a minimal, webhook-focused config.
- Chores
- Added persistence and caching for the default “No team” configuration.
- Introduced a database table to store the default configuration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-08-28 21:38:27 +00:00
t . Logf ( "[TestHostMDMAppleProfilesStatus] Starting SECOND cron run (after h3, h4 enrolled in team1) at %s" , time . Now ( ) . Format ( time . RFC3339 ) )
2024-04-29 19:43:15 +00:00
s . awaitTriggerProfileSchedule ( t )
Allow configuring webhook policy automations for "No team" (#32129)
Fixes #32060
This PR adds:
- new default_team_config_json table
- caching of config from that table, including deep copy methods -- all
of this is not absolutely needed for this change since we are only using
`webhook_settings.failing_policies_webhook` here but added for
completeness/future
- teams/0 API updates
- GitOps updates
- generate gitops updates
Future PRs will add:
- ticket automation
- primo mode migration
- frontend changes
- documentation
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked table schema to confirm autoupdate
## New Fleet configuration settings
- [x] Verified that the setting is exported via `fleetctl
generate-gitops`
- [x] Verified that the setting is cleared on the server if it is not
supplied in a YAML file (or that it is documented as being optional)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- New Features
- Configure failing-policy webhooks for “No team” via GitOps
(no-team.yml) and API, including enable/disable, destination URL, policy
IDs, and batch size; settings clear when omitted.
- GitOps and CLI now read/apply the real “No team” settings with dry-run
support.
- Policy automation evaluates hosts without a team and triggers “No
team” webhooks when applicable.
- GET/PATCH team 0 returns/accepts a minimal, webhook-focused config.
- Chores
- Added persistence and caching for the default “No team” configuration.
- Introduced a database table to store the default configuration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-08-28 21:38:27 +00:00
t . Logf ( "[TestHostMDMAppleProfilesStatus] SECOND cron run completed at %s" , time . Now ( ) . Format ( time . RFC3339 ) )
2025-06-18 17:59:12 +00:00
// T1.3 is user-scoped and the h4 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)
2024-07-03 14:20:33 +00:00
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
2024-04-18 21:01:37 +00:00
h3 : {
{ Identifier : "T1.1" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "T1.2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
2025-07-02 14:54:54 +00:00
{ Identifier : "T1.3.user" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
2024-04-29 19:43:15 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
2024-04-18 21:01:37 +00:00
} ,
h4 : {
{ Identifier : "T1.1" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "T1.2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
2025-07-02 14:54:54 +00:00
{ Identifier : "T1.3.user" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
2024-04-29 19:43:15 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
2024-04-18 21:01:37 +00:00
} ,
} )
// apply the pending profiles
2025-06-18 17:59:12 +00:00
triggerReconcileProfilesMarkVerifying ( )
2024-04-18 21:01:37 +00:00
2025-06-18 17:59:12 +00:00
// Verify there is a command on the user channel for h3
2025-06-16 20:46:38 +00:00
enrollmentIds , err = s . ds . GetEnrollmentIDsWithPendingMDMAppleCommands ( ctx )
require . NoError ( t , err )
assert . Contains ( t , enrollmentIds , h3UserEnrollment . ID )
assert . Contains ( t , enrollmentIds , h3 . UUID )
assert . Contains ( t , enrollmentIds , h4 . UUID )
2024-04-18 21:01:37 +00:00
// switch a no team host (h1) to a team (tm2)
Allow configuring webhook policy automations for "No team" (#32129)
Fixes #32060
This PR adds:
- new default_team_config_json table
- caching of config from that table, including deep copy methods -- all
of this is not absolutely needed for this change since we are only using
`webhook_settings.failing_policies_webhook` here but added for
completeness/future
- teams/0 API updates
- GitOps updates
- generate gitops updates
Future PRs will add:
- ticket automation
- primo mode migration
- frontend changes
- documentation
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked table schema to confirm autoupdate
## New Fleet configuration settings
- [x] Verified that the setting is exported via `fleetctl
generate-gitops`
- [x] Verified that the setting is cleared on the server if it is not
supplied in a YAML file (or that it is documented as being optional)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- New Features
- Configure failing-policy webhooks for “No team” via GitOps
(no-team.yml) and API, including enable/disable, destination URL, policy
IDs, and batch size; settings clear when omitted.
- GitOps and CLI now read/apply the real “No team” settings with dry-run
support.
- Policy automation evaluates hosts without a team and triggers “No
team” webhooks when applicable.
- GET/PATCH team 0 returns/accepts a minimal, webhook-focused config.
- Chores
- Added persistence and caching for the default “No team” configuration.
- Introduced a database table to store the default configuration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-08-28 21:38:27 +00:00
t . Logf ( "[TestHostMDMAppleProfilesStatus] Transferring host h1 (id=%d) from no team to team tm2 (id=%d) at %s" , h1 . ID , tm2 . ID , time . Now ( ) . Format ( time . RFC3339 ) )
2024-04-18 21:01:37 +00:00
var moveHostResp addHostsToTeamResponse
s . DoJSON ( "POST" , "/api/v1/fleet/hosts/transfer" ,
addHostsToTeamRequest { TeamID : & tm2 . ID , HostIDs : [ ] uint { h1 . ID } } , http . StatusOK , & moveHostResp )
Allow configuring webhook policy automations for "No team" (#32129)
Fixes #32060
This PR adds:
- new default_team_config_json table
- caching of config from that table, including deep copy methods -- all
of this is not absolutely needed for this change since we are only using
`webhook_settings.failing_policies_webhook` here but added for
completeness/future
- teams/0 API updates
- GitOps updates
- generate gitops updates
Future PRs will add:
- ticket automation
- primo mode migration
- frontend changes
- documentation
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked table schema to confirm autoupdate
## New Fleet configuration settings
- [x] Verified that the setting is exported via `fleetctl
generate-gitops`
- [x] Verified that the setting is cleared on the server if it is not
supplied in a YAML file (or that it is documented as being optional)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- New Features
- Configure failing-policy webhooks for “No team” via GitOps
(no-team.yml) and API, including enable/disable, destination URL, policy
IDs, and batch size; settings clear when omitted.
- GitOps and CLI now read/apply the real “No team” settings with dry-run
support.
- Policy automation evaluates hosts without a team and triggers “No
team” webhooks when applicable.
- GET/PATCH team 0 returns/accepts a minimal, webhook-focused config.
- Chores
- Added persistence and caching for the default “No team” configuration.
- Introduced a database table to store the default configuration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-08-28 21:38:27 +00:00
t . Logf ( "[TestHostMDMAppleProfilesStatus] Host h1 transfer completed at %s" , time . Now ( ) . Format ( time . RFC3339 ) )
2024-07-03 14:20:33 +00:00
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
2024-04-18 21:01:37 +00:00
h1 : {
{ Identifier : "G1" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "G2" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
2025-07-02 14:54:54 +00:00
{ Identifier : "G3.user" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
2024-04-18 21:01:37 +00:00
{ 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 } ,
} ,
h2 : {
{ Identifier : "G1" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : "G2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2025-07-02 14:54:54 +00:00
{ Identifier : "G3.user" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
2024-04-18 21:01:37 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
} )
// switch a team host (h3) to another team (tm2)
s . DoJSON ( "POST" , "/api/v1/fleet/hosts/transfer" ,
addHostsToTeamRequest { TeamID : & tm2 . ID , HostIDs : [ ] uint { h3 . ID } } , http . StatusOK , & moveHostResp )
2025-06-18 17:59:12 +00:00
// create the user-enrollment for host h4
err = h4Device . UserEnroll ( )
require . NoError ( t , err )
// run the cron
Allow configuring webhook policy automations for "No team" (#32129)
Fixes #32060
This PR adds:
- new default_team_config_json table
- caching of config from that table, including deep copy methods -- all
of this is not absolutely needed for this change since we are only using
`webhook_settings.failing_policies_webhook` here but added for
completeness/future
- teams/0 API updates
- GitOps updates
- generate gitops updates
Future PRs will add:
- ticket automation
- primo mode migration
- frontend changes
- documentation
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked table schema to confirm autoupdate
## New Fleet configuration settings
- [x] Verified that the setting is exported via `fleetctl
generate-gitops`
- [x] Verified that the setting is cleared on the server if it is not
supplied in a YAML file (or that it is documented as being optional)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- New Features
- Configure failing-policy webhooks for “No team” via GitOps
(no-team.yml) and API, including enable/disable, destination URL, policy
IDs, and batch size; settings clear when omitted.
- GitOps and CLI now read/apply the real “No team” settings with dry-run
support.
- Policy automation evaluates hosts without a team and triggers “No
team” webhooks when applicable.
- GET/PATCH team 0 returns/accepts a minimal, webhook-focused config.
- Chores
- Added persistence and caching for the default “No team” configuration.
- Introduced a database table to store the default configuration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-08-28 21:38:27 +00:00
t . Logf ( "[TestHostMDMAppleProfilesStatus] Starting THIRD cron run (after h3->tm2, h4 user enrolled) at %s" , time . Now ( ) . Format ( time . RFC3339 ) )
2025-06-18 17:59:12 +00:00
s . awaitTriggerProfileSchedule ( t )
Allow configuring webhook policy automations for "No team" (#32129)
Fixes #32060
This PR adds:
- new default_team_config_json table
- caching of config from that table, including deep copy methods -- all
of this is not absolutely needed for this change since we are only using
`webhook_settings.failing_policies_webhook` here but added for
completeness/future
- teams/0 API updates
- GitOps updates
- generate gitops updates
Future PRs will add:
- ticket automation
- primo mode migration
- frontend changes
- documentation
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked table schema to confirm autoupdate
## New Fleet configuration settings
- [x] Verified that the setting is exported via `fleetctl
generate-gitops`
- [x] Verified that the setting is cleared on the server if it is not
supplied in a YAML file (or that it is documented as being optional)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- New Features
- Configure failing-policy webhooks for “No team” via GitOps
(no-team.yml) and API, including enable/disable, destination URL, policy
IDs, and batch size; settings clear when omitted.
- GitOps and CLI now read/apply the real “No team” settings with dry-run
support.
- Policy automation evaluates hosts without a team and triggers “No
team” webhooks when applicable.
- GET/PATCH team 0 returns/accepts a minimal, webhook-focused config.
- Chores
- Added persistence and caching for the default “No team” configuration.
- Introduced a database table to store the default configuration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-08-28 21:38:27 +00:00
t . Logf ( "[TestHostMDMAppleProfilesStatus] THIRD cron run completed at %s" , time . Now ( ) . Format ( time . RFC3339 ) )
2024-07-03 14:20:33 +00:00
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
2024-04-18 21:01:37 +00:00
h3 : {
{ Identifier : "T1.1" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "T1.2" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
2025-07-02 14:54:54 +00:00
{ Identifier : "T1.3.user" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
2024-04-18 21:01:37 +00:00
{ 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 } ,
} ,
h4 : {
{ Identifier : "T1.1" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : "T1.2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2025-07-02 14:54:54 +00:00
{ Identifier : "T1.3.user" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
2024-04-18 21:01:37 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
} )
// switch a team host (h4) to no team
Allow configuring webhook policy automations for "No team" (#32129)
Fixes #32060
This PR adds:
- new default_team_config_json table
- caching of config from that table, including deep copy methods -- all
of this is not absolutely needed for this change since we are only using
`webhook_settings.failing_policies_webhook` here but added for
completeness/future
- teams/0 API updates
- GitOps updates
- generate gitops updates
Future PRs will add:
- ticket automation
- primo mode migration
- frontend changes
- documentation
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked table schema to confirm autoupdate
## New Fleet configuration settings
- [x] Verified that the setting is exported via `fleetctl
generate-gitops`
- [x] Verified that the setting is cleared on the server if it is not
supplied in a YAML file (or that it is documented as being optional)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- New Features
- Configure failing-policy webhooks for “No team” via GitOps
(no-team.yml) and API, including enable/disable, destination URL, policy
IDs, and batch size; settings clear when omitted.
- GitOps and CLI now read/apply the real “No team” settings with dry-run
support.
- Policy automation evaluates hosts without a team and triggers “No
team” webhooks when applicable.
- GET/PATCH team 0 returns/accepts a minimal, webhook-focused config.
- Chores
- Added persistence and caching for the default “No team” configuration.
- Introduced a database table to store the default configuration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-08-28 21:38:27 +00:00
t . Logf ( "[TestHostMDMAppleProfilesStatus] Transferring host h4 (id=%d) from team tm1 to NO TEAM (nil) at %s" , h4 . ID , time . Now ( ) . Format ( time . RFC3339 ) )
2024-04-18 21:01:37 +00:00
s . DoJSON ( "POST" , "/api/v1/fleet/hosts/transfer" ,
addHostsToTeamRequest { TeamID : nil , HostIDs : [ ] uint { h4 . ID } } , http . StatusOK , & moveHostResp )
Allow configuring webhook policy automations for "No team" (#32129)
Fixes #32060
This PR adds:
- new default_team_config_json table
- caching of config from that table, including deep copy methods -- all
of this is not absolutely needed for this change since we are only using
`webhook_settings.failing_policies_webhook` here but added for
completeness/future
- teams/0 API updates
- GitOps updates
- generate gitops updates
Future PRs will add:
- ticket automation
- primo mode migration
- frontend changes
- documentation
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked table schema to confirm autoupdate
## New Fleet configuration settings
- [x] Verified that the setting is exported via `fleetctl
generate-gitops`
- [x] Verified that the setting is cleared on the server if it is not
supplied in a YAML file (or that it is documented as being optional)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- New Features
- Configure failing-policy webhooks for “No team” via GitOps
(no-team.yml) and API, including enable/disable, destination URL, policy
IDs, and batch size; settings clear when omitted.
- GitOps and CLI now read/apply the real “No team” settings with dry-run
support.
- Policy automation evaluates hosts without a team and triggers “No
team” webhooks when applicable.
- GET/PATCH team 0 returns/accepts a minimal, webhook-focused config.
- Chores
- Added persistence and caching for the default “No team” configuration.
- Introduced a database table to store the default configuration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-08-28 21:38:27 +00:00
t . Logf ( "[TestHostMDMAppleProfilesStatus] Host h4 transfer to NO TEAM completed at %s" , time . Now ( ) . Format ( time . RFC3339 ) )
2024-07-03 14:20:33 +00:00
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
2024-04-18 21:01:37 +00:00
h3 : {
{ Identifier : "T1.1" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "T1.2" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
2025-07-02 14:54:54 +00:00
{ Identifier : "T1.3.user" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
2024-04-18 21:01:37 +00:00
{ 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 } ,
} ,
h4 : {
{ Identifier : "T1.1" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "T1.2" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
2025-07-02 14:54:54 +00:00
{ Identifier : "T1.3.user" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } , // still pending install due to cron not having run
2024-04-18 21:01:37 +00:00
{ Identifier : "G1" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "G2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
2025-07-02 14:54:54 +00:00
{ Identifier : "G3.user" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
2024-04-18 21:01:37 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
} )
// apply the pending profiles
2025-06-18 17:59:12 +00:00
triggerReconcileProfilesMarkVerifying ( )
2024-04-18 21:01:37 +00:00
// add a profile to no team (h2 and h4 are now part of no team)
body , headers := generateNewProfileMultipartRequest ( t ,
2025-06-16 20:46:38 +00:00
"some_name" , mobileconfigForTest ( "G4" , "G4" ) , s . token , nil )
2024-04-18 21:01:37 +00:00
s . DoRawWithHeaders ( "POST" , "/api/latest/fleet/mdm/apple/profiles" , body . Bytes ( ) , http . StatusOK , headers )
2024-07-03 14:20:33 +00:00
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
2025-06-30 13:22:34 +00:00
h2 : { // still no user channel
2024-04-18 21:01:37 +00:00
{ Identifier : "G1" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : "G2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2025-07-02 14:54:54 +00:00
{ Identifier : "G3.user" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
2024-04-18 21:01:37 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2025-06-16 20:46:38 +00:00
{ Identifier : "G4" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
2024-04-18 21:01:37 +00:00
} ,
h4 : {
{ Identifier : "G1" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : "G2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2025-07-02 14:54:54 +00:00
{ Identifier : "G3.user" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2024-04-18 21:01:37 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2025-06-16 20:46:38 +00:00
{ Identifier : "G4" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
2024-04-18 21:01:37 +00:00
} ,
} )
// add a profile to team 2 (h1 and h3 are now part of team 2)
body , headers = generateNewProfileMultipartRequest ( t ,
"some_name" , mobileconfigForTest ( "T2.2" , "T2.2" ) , s . token , map [ string ] [ ] string { "team_id" : { fmt . Sprintf ( "%d" , tm2 . ID ) } } )
s . DoRawWithHeaders ( "POST" , "/api/latest/fleet/mdm/apple/profiles" , body . Bytes ( ) , http . StatusOK , headers )
2024-07-03 14:20:33 +00:00
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
2024-04-18 21:01:37 +00:00
h1 : {
{ Identifier : "T2.1" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : "T2.2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
h3 : {
{ Identifier : "T2.1" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : "T2.2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
} )
// apply the pending profiles
2025-06-18 17:59:12 +00:00
triggerReconcileProfilesMarkVerifying ( )
2024-04-18 21:01:37 +00:00
// delete a no team profile
noTeamProfs , err := s . ds . ListMDMAppleConfigProfiles ( ctx , nil )
require . NoError ( t , err )
var g1ProfID uint
for _ , p := range noTeamProfs {
if p . Identifier == "G1" {
g1ProfID = p . ProfileID
break
}
}
require . NotZero ( t , g1ProfID )
var delProfResp deleteMDMAppleConfigProfileResponse
s . DoJSON ( "DELETE" , fmt . Sprintf ( "/api/latest/fleet/mdm/apple/profiles/%d" , g1ProfID ) ,
deleteMDMAppleConfigProfileRequest { } , http . StatusOK , & delProfResp )
2024-07-03 14:20:33 +00:00
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
2024-04-18 21:01:37 +00:00
h2 : {
{ Identifier : "G1" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "G2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2025-07-02 14:54:54 +00:00
{ Identifier : "G3.user" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
2025-06-16 20:46:38 +00:00
{ Identifier : "G4" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2024-04-18 21:01:37 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
h4 : {
{ Identifier : "G1" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "G2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2025-07-02 14:54:54 +00:00
{ Identifier : "G3.user" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2025-06-16 20:46:38 +00:00
{ Identifier : "G4" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2024-04-18 21:01:37 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
} )
// delete a team profile
tm2Profs , err := s . ds . ListMDMAppleConfigProfiles ( ctx , & tm2 . ID )
require . NoError ( t , err )
var tm21ProfID uint
for _ , p := range tm2Profs {
if p . Identifier == "T2.1" {
tm21ProfID = p . ProfileID
break
}
}
require . NotZero ( t , tm21ProfID )
s . DoJSON ( "DELETE" , fmt . Sprintf ( "/api/latest/fleet/mdm/apple/profiles/%d" , tm21ProfID ) ,
deleteMDMAppleConfigProfileRequest { } , http . StatusOK , & delProfResp )
2024-07-03 14:20:33 +00:00
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
2024-04-18 21:01:37 +00:00
h1 : {
{ Identifier : "T2.1" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "T2.2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
h3 : {
{ Identifier : "T2.1" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "T2.2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
} )
// apply the pending profiles
2025-06-18 17:59:12 +00:00
triggerReconcileProfilesMarkVerifying ( )
2024-04-18 21:01:37 +00:00
// bulk-set profiles for no team, with add/delete/edit
g2Edited := mobileconfigForTest ( "G2b" , "G2b" )
2025-06-16 20:46:38 +00:00
g5Content := mobileconfigForTest ( "G5" , "G5" )
2024-04-18 21:01:37 +00:00
s . Do ( "POST" , "/api/latest/fleet/mdm/apple/profiles/batch" ,
batchSetMDMAppleProfilesRequest {
Profiles : [ ] [ ] byte {
g2Edited ,
// G3 is deleted
2025-06-16 20:46:38 +00:00
// G4 is deleted
g5Content ,
2024-04-18 21:01:37 +00:00
} ,
} , http . StatusNoContent )
2024-07-03 14:20:33 +00:00
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
2024-04-18 21:01:37 +00:00
h2 : {
{ Identifier : "G2" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "G2b" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
2025-06-16 20:46:38 +00:00
{ Identifier : "G4" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "G5" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
2024-04-18 21:01:37 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
h4 : {
{ Identifier : "G2" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "G2b" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
2025-07-02 14:54:54 +00:00
{ Identifier : "G3.user" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
2025-06-16 20:46:38 +00:00
{ Identifier : "G4" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "G5" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
2024-04-18 21:01:37 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
} )
// bulk-set profiles for a team, with add/delete/edit
t22Edited := mobileconfigForTest ( "T2.2b" , "T2.2b" )
t23Content := mobileconfigForTest ( "T2.3" , "T2.3" )
s . Do ( "POST" , "/api/latest/fleet/mdm/apple/profiles/batch" ,
batchSetMDMAppleProfilesRequest {
Profiles : [ ] [ ] byte {
t22Edited ,
t23Content ,
} ,
} , http . StatusNoContent , "team_id" , fmt . Sprint ( tm2 . ID ) )
2024-07-03 14:20:33 +00:00
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
2024-04-18 21:01:37 +00:00
h1 : {
{ Identifier : "T2.2" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "T2.2b" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "T2.3" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
h3 : {
{ Identifier : "T2.2" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "T2.2b" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "T2.3" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
} )
// apply the pending profiles
2025-06-18 17:59:12 +00:00
triggerReconcileProfilesMarkVerifying ( )
2024-04-18 21:01:37 +00:00
// bulk-set profiles for no team and team 2, without changes, and team 1 added (but no host affected)
s . Do ( "POST" , "/api/latest/fleet/mdm/apple/profiles/batch" ,
batchSetMDMAppleProfilesRequest {
Profiles : [ ] [ ] byte {
g2Edited ,
2025-06-16 20:46:38 +00:00
g5Content ,
2024-04-18 21:01:37 +00:00
} ,
} , http . StatusNoContent )
s . Do ( "POST" , "/api/latest/fleet/mdm/apple/profiles/batch" ,
batchSetMDMAppleProfilesRequest {
Profiles : [ ] [ ] byte {
t22Edited ,
t23Content ,
} ,
} , http . StatusNoContent , "team_id" , fmt . Sprint ( tm2 . ID ) )
s . Do ( "POST" , "/api/latest/fleet/mdm/apple/profiles/batch" ,
batchSetMDMAppleProfilesRequest {
Profiles : [ ] [ ] byte {
2025-07-02 14:54:54 +00:00
scopedMobileconfigForTest ( "T1.3" , "T1.3.user" , & payloadScopeUser ) ,
2024-04-18 21:01:37 +00:00
} ,
} , http . StatusNoContent , "team_id" , fmt . Sprint ( tm1 . ID ) )
2024-07-03 14:20:33 +00:00
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
2024-04-18 21:01:37 +00:00
h1 : {
{ Identifier : "T2.2b" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : "T2.3" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
h2 : {
{ Identifier : "G2b" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2025-06-16 20:46:38 +00:00
{ Identifier : "G5" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2024-04-18 21:01:37 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
h3 : {
{ Identifier : "T2.2b" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : "T2.3" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
h4 : {
{ Identifier : "G2b" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2025-06-16 20:46:38 +00:00
{ Identifier : "G5" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2024-04-18 21:01:37 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
} )
// delete team 2 (h1 and h3 are part of that team)
s . Do ( "DELETE" , fmt . Sprintf ( "/api/latest/fleet/teams/%d" , tm2 . ID ) , nil , http . StatusOK )
2024-07-03 14:20:33 +00:00
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
2024-04-18 21:01:37 +00:00
h1 : {
{ Identifier : "T2.2b" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "T2.3" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "G2b" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
2025-06-16 20:46:38 +00:00
{ Identifier : "G5" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
2024-04-18 21:01:37 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
h3 : {
{ Identifier : "T2.2b" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "T2.3" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "G2b" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
2025-06-16 20:46:38 +00:00
{ Identifier : "G5" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
2024-04-18 21:01:37 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
} )
// apply the pending profiles
2025-06-18 17:59:12 +00:00
triggerReconcileProfilesMarkVerifying ( )
2024-04-18 21:01:37 +00:00
// all profiles now verifying
2024-07-03 14:20:33 +00:00
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
2024-04-18 21:01:37 +00:00
h1 : {
{ Identifier : "G2b" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2025-06-16 20:46:38 +00:00
{ Identifier : "G5" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2024-04-18 21:01:37 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
h2 : {
{ Identifier : "G2b" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2025-06-16 20:46:38 +00:00
{ Identifier : "G5" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2024-04-18 21:01:37 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
h3 : {
{ Identifier : "G2b" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2025-06-16 20:46:38 +00:00
{ Identifier : "G5" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2024-04-18 21:01:37 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
h4 : {
{ Identifier : "G2b" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2025-06-16 20:46:38 +00:00
{ Identifier : "G5" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2024-04-18 21:01:37 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
} )
// h1 verified one of the profiles
require . NoError ( t , apple_mdm . VerifyHostMDMProfiles ( context . Background ( ) , s . ds , h1 , map [ string ] * fleet . HostMacOSProfile {
"G2b" : { Identifier : "G2b" , DisplayName : "G2b" , InstallDate : time . Now ( ) } ,
} ) )
2024-07-03 14:20:33 +00:00
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
2024-04-18 21:01:37 +00:00
h1 : {
{ Identifier : "G2b" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerified } ,
2025-06-16 20:46:38 +00:00
{ Identifier : "G5" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2024-04-18 21:01:37 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
h2 : {
{ Identifier : "G2b" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2025-06-16 20:46:38 +00:00
{ Identifier : "G5" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2024-04-18 21:01:37 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
h3 : {
{ Identifier : "G2b" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2025-06-16 20:46:38 +00:00
{ Identifier : "G5" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2024-04-18 21:01:37 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
h4 : {
{ Identifier : "G2b" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2025-06-16 20:46:38 +00:00
{ Identifier : "G5" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2024-04-18 21:01:37 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
} )
// switch a team host (h1) to another team (tm1)
assignHostToTeam ( h1 , & tm1 . ID )
// Create a new profile that will be labeled
body , headers = generateNewProfileMultipartRequest (
t ,
"label_prof" ,
mobileconfigForTest ( "label_prof" , "label_prof" ) ,
s . token ,
map [ string ] [ ] string { "team_id" : { fmt . Sprintf ( "%d" , tm1 . ID ) } } ,
)
s . DoRawWithHeaders ( "POST" , "/api/latest/fleet/mdm/apple/profiles" , body . Bytes ( ) , http . StatusOK , headers )
var uid string
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
return sqlx . GetContext ( ctx , q , & uid , ` SELECT profile_uuid FROM mdm_apple_configuration_profiles WHERE identifier = ? ` , "label_prof" )
} )
label , err := s . ds . NewLabel ( ctx , & fleet . Label { Name : "test label 1" , Query : "select 1;" } )
require . NoError ( t , err )
// Update label with host membership
mysql . ExecAdhocSQL (
t , s . ds , func ( db sqlx . ExtContext ) error {
_ , err := db . ExecContext (
context . Background ( ) ,
"INSERT IGNORE INTO label_membership (host_id, label_id) VALUES (?, ?)" ,
h1 . ID ,
label . ID ,
)
return err
} ,
)
// Update profile <-> label mapping
mysql . ExecAdhocSQL (
t , s . ds , func ( db sqlx . ExtContext ) error {
_ , err := db . ExecContext (
context . Background ( ) ,
"INSERT INTO mdm_configuration_profile_labels (apple_profile_uuid, label_name, label_id) VALUES (?, ?, ?)" ,
uid ,
label . Name ,
label . ID ,
)
return err
} ,
)
2025-06-18 17:59:12 +00:00
triggerReconcileProfilesMarkVerifying ( )
2024-04-18 21:01:37 +00:00
2024-07-03 14:20:33 +00:00
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
2024-04-18 21:01:37 +00:00
h1 : {
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2025-07-02 14:54:54 +00:00
{ Identifier : "T1.3.user" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2024-04-18 21:01:37 +00:00
{ Identifier : "label_prof" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
h2 : {
{ Identifier : "G2b" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2025-06-16 20:46:38 +00:00
{ Identifier : "G5" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2024-04-18 21:01:37 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
h3 : {
{ Identifier : "G2b" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2025-06-16 20:46:38 +00:00
{ Identifier : "G5" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2024-04-18 21:01:37 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
h4 : {
{ Identifier : "G2b" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2025-06-16 20:46:38 +00:00
{ Identifier : "G5" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2024-04-18 21:01:37 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
} )
require . NoError ( t , apple_mdm . VerifyHostMDMProfiles ( context . Background ( ) , s . ds , h1 , map [ string ] * fleet . HostMacOSProfile {
"label_prof" : { Identifier : "label_prof" , DisplayName : "label_prof" , InstallDate : time . Now ( ) } ,
} ) )
2024-07-03 14:20:33 +00:00
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
2024-04-18 21:01:37 +00:00
h1 : {
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2025-07-02 14:54:54 +00:00
{ Identifier : "T1.3.user" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2024-04-18 21:01:37 +00:00
{ Identifier : "label_prof" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerified } ,
} ,
h2 : {
{ Identifier : "G2b" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2025-06-16 20:46:38 +00:00
{ Identifier : "G5" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2024-04-18 21:01:37 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
h3 : {
{ Identifier : "G2b" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2025-06-16 20:46:38 +00:00
{ Identifier : "G5" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2024-04-18 21:01:37 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
h4 : {
{ Identifier : "G2b" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2025-06-16 20:46:38 +00:00
{ Identifier : "G5" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
2024-04-18 21:01:37 +00:00
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
} )
}
func ( s * integrationMDMTestSuite ) TestMDMConfigProfileCRUD ( ) {
t := s . T ( )
ctx := context . Background ( )
testTeam , err := s . ds . NewTeam ( ctx , & fleet . Team { Name : "TestTeam" } )
require . NoError ( t , err )
2025-11-25 12:39:06 +00:00
// Ensure MDM is turned on
appConfig , err := s . ds . AppConfig ( ctx )
require . NoError ( t , err )
appConfig . MDM . AndroidEnabledAndConfigured = true
appConfig . MDM . EnabledAndConfigured = true
appConfig . MDM . WindowsEnabledAndConfigured = true
err = s . ds . SaveAppConfig ( ctx , appConfig )
require . NoError ( t , err )
2024-06-25 19:26:28 +00:00
// NOTE: label names starting with "-" are sent as "labels_excluding_any"
// (and the leading "-" is removed from the name). Names starting with
// "!" are sent as the deprecated "labels" field (and the "!" is removed).
2024-11-14 14:30:32 +00:00
// Names starting with a "~" prefix are sent as "labels_include_any"
// (and the leading "~" is removed.
2024-06-25 19:26:28 +00:00
addLabelsFields := func ( labelNames [ ] string ) map [ string ] [ ] string {
2024-11-14 14:30:32 +00:00
var deprLabels , inclAllLabels , inclAnyLabels , exclLabels [ ] string
2024-06-25 19:26:28 +00:00
for _ , lbl := range labelNames {
2024-11-14 14:30:32 +00:00
switch {
case strings . HasPrefix ( lbl , "~" ) :
inclAnyLabels = append ( inclAnyLabels , strings . TrimPrefix ( lbl , "~" ) )
case strings . HasPrefix ( lbl , "-" ) :
2024-06-25 19:26:28 +00:00
exclLabels = append ( exclLabels , strings . TrimPrefix ( lbl , "-" ) )
2024-11-14 14:30:32 +00:00
case strings . HasPrefix ( lbl , "!" ) :
2024-06-25 19:26:28 +00:00
deprLabels = append ( deprLabels , strings . TrimPrefix ( lbl , "!" ) )
2024-11-14 14:30:32 +00:00
default :
inclAllLabels = append ( inclAllLabels , lbl )
2024-06-25 19:26:28 +00:00
}
2024-04-18 21:01:37 +00:00
}
2024-06-25 19:26:28 +00:00
fields := make ( map [ string ] [ ] string )
if len ( deprLabels ) > 0 {
fields [ "labels" ] = deprLabels
}
2024-11-14 14:30:32 +00:00
if len ( inclAllLabels ) > 0 {
fields [ "labels_include_all" ] = inclAllLabels
2024-06-25 19:26:28 +00:00
}
if len ( exclLabels ) > 0 {
fields [ "labels_exclude_any" ] = exclLabels
}
2024-11-14 14:30:32 +00:00
if len ( inclAnyLabels ) > 0 {
fields [ "labels_include_any" ] = inclAnyLabels
}
2024-06-25 19:26:28 +00:00
return fields
}
assertAppleProfile := func ( filename , name , ident string , teamID uint , labelNames [ ] string , wantStatus int , wantErrMsg string ) string {
fields := addLabelsFields ( labelNames )
2024-04-18 21:01:37 +00:00
if teamID > 0 {
fields [ "team_id" ] = [ ] string { fmt . Sprintf ( "%d" , teamID ) }
}
body , headers := generateNewProfileMultipartRequest (
t , filename , mobileconfigForTest ( name , ident ) , s . token , fields ,
)
res := s . DoRawWithHeaders ( "POST" , "/api/latest/fleet/configuration_profiles" , body . Bytes ( ) , wantStatus , headers )
if wantErrMsg != "" {
errMsg := extractServerErrorText ( res . Body )
require . Contains ( t , errMsg , wantErrMsg )
return ""
}
var resp newMDMConfigProfileResponse
err := json . NewDecoder ( res . Body ) . Decode ( & resp )
require . NoError ( t , err )
require . NotEmpty ( t , resp . ProfileUUID )
require . Equal ( t , "a" , string ( resp . ProfileUUID [ 0 ] ) )
return resp . ProfileUUID
}
assertAppleDeclaration := func ( filename , ident string , teamID uint , labelNames [ ] string , wantStatus int , wantErrMsg string ) string {
2024-06-25 19:26:28 +00:00
fields := addLabelsFields ( labelNames )
2024-04-18 21:01:37 +00:00
if teamID > 0 {
fields [ "team_id" ] = [ ] string { fmt . Sprintf ( "%d" , teamID ) }
}
bytes := [ ] byte ( fmt . Sprintf ( ` {
"Type" : "com.apple.configuration.foo" ,
"Payload" : {
"Echo" : "f1337"
} ,
"Identifier" : "%s"
} ` , ident ) )
body , headers := generateNewProfileMultipartRequest ( t , filename , bytes , s . token , fields )
res := s . DoRawWithHeaders ( "POST" , "/api/latest/fleet/configuration_profiles" , body . Bytes ( ) , wantStatus , headers )
if wantErrMsg != "" {
errMsg := extractServerErrorText ( res . Body )
require . Contains ( t , errMsg , wantErrMsg )
return ""
}
var resp newMDMConfigProfileResponse
err := json . NewDecoder ( res . Body ) . Decode ( & resp )
require . NoError ( t , err )
require . NotEmpty ( t , resp . ProfileUUID )
require . Equal ( t , fleet . MDMAppleDeclarationUUIDPrefix , string ( resp . ProfileUUID [ 0 ] ) )
return resp . ProfileUUID
}
createAppleProfile := func ( name , ident string , teamID uint , labelNames [ ] string ) string {
uid := assertAppleProfile ( name + ".mobileconfig" , name , ident , teamID , labelNames , http . StatusOK , "" )
var wantJSON string
if teamID == 0 {
wantJSON = fmt . Sprintf ( ` { "team_id": null, "team_name": null, "profile_name": %q, "profile_identifier": %q} ` , name , ident )
} else {
wantJSON = fmt . Sprintf ( ` { "team_id": %d, "team_name": %q, "profile_name": %q, "profile_identifier": %q} ` , teamID , testTeam . Name , name , ident )
}
s . lastActivityOfTypeMatches ( fleet . ActivityTypeCreatedMacosProfile { } . ActivityName ( ) , wantJSON , 0 )
return uid
}
createAppleDeclaration := func ( name , ident string , teamID uint , labelNames [ ] string ) string {
uid := assertAppleDeclaration ( name + ".json" , ident , teamID , labelNames , http . StatusOK , "" )
var wantJSON string
if teamID == 0 {
wantJSON = fmt . Sprintf ( ` { "team_id": null, "team_name": null, "profile_name": %q, "identifier": %q} ` , name , ident )
} else {
wantJSON = fmt . Sprintf ( ` { "team_id": %d, "team_name": %q, "profile_name": %q, "identifier": %q} ` , teamID , testTeam . Name , name , ident )
}
s . lastActivityOfTypeMatches ( fleet . ActivityTypeCreatedDeclarationProfile { } . ActivityName ( ) , wantJSON , 0 )
return uid
}
assertWindowsProfile := func ( filename , locURI string , teamID uint , labelNames [ ] string , wantStatus int , wantErrMsg string ) string {
2024-06-25 19:26:28 +00:00
fields := addLabelsFields ( labelNames )
2024-04-18 21:01:37 +00:00
if teamID > 0 {
fields [ "team_id" ] = [ ] string { fmt . Sprintf ( "%d" , teamID ) }
}
body , headers := generateNewProfileMultipartRequest (
t ,
filename ,
[ ] byte ( fmt . Sprintf ( ` <Add><Item><Target><LocURI>%s</LocURI></Target></Item></Add><Replace><Item><Target><LocURI>%s</LocURI></Target></Item></Replace> ` , locURI , locURI ) ) ,
s . token ,
fields ,
)
res := s . DoRawWithHeaders ( "POST" , "/api/latest/fleet/configuration_profiles" , body . Bytes ( ) , wantStatus , headers )
if wantErrMsg != "" {
errMsg := extractServerErrorText ( res . Body )
require . Contains ( t , errMsg , wantErrMsg )
return ""
}
var resp newMDMConfigProfileResponse
err := json . NewDecoder ( res . Body ) . Decode ( & resp )
require . NoError ( t , err )
require . NotEmpty ( t , resp . ProfileUUID )
require . Equal ( t , "w" , string ( resp . ProfileUUID [ 0 ] ) )
return resp . ProfileUUID
}
createWindowsProfile := func ( name string , teamID uint , labels [ ] string ) string {
uid := assertWindowsProfile ( name + ".xml" , "./Test" , teamID , labels , http . StatusOK , "" )
var wantJSON string
if teamID == 0 {
wantJSON = fmt . Sprintf ( ` { "team_id": null, "team_name": null, "profile_name": %q} ` , name )
} else {
wantJSON = fmt . Sprintf ( ` { "team_id": %d, "team_name": %q, "profile_name": %q} ` , teamID , testTeam . Name , name )
}
s . lastActivityOfTypeMatches ( fleet . ActivityTypeCreatedWindowsProfile { } . ActivityName ( ) , wantJSON , 0 )
return uid
}
2025-09-22 15:29:57 +00:00
assertAndroidProfile := func ( filename string , teamID uint , labelNames [ ] string , wantStatus int , wantErrMsg string ) string {
fields := addLabelsFields ( labelNames )
if teamID > 0 {
fields [ "team_id" ] = [ ] string { fmt . Sprintf ( "%d" , teamID ) }
}
bytes := [ ] byte ( ` {
"removeUserDisabled" : false
} ` )
body , headers := generateNewProfileMultipartRequest ( t , filename , bytes , s . token , fields )
res := s . DoRawWithHeaders ( "POST" , "/api/latest/fleet/configuration_profiles" , body . Bytes ( ) , wantStatus , headers )
if wantErrMsg != "" {
errMsg := extractServerErrorText ( res . Body )
require . Contains ( t , errMsg , wantErrMsg )
return ""
}
var resp newMDMConfigProfileResponse
err := json . NewDecoder ( res . Body ) . Decode ( & resp )
require . NoError ( t , err )
require . NotEmpty ( t , resp . ProfileUUID )
require . Equal ( t , fleet . MDMAndroidProfileUUIDPrefix , string ( resp . ProfileUUID [ 0 ] ) )
return resp . ProfileUUID
}
createAndroidProfile := func ( name string , teamID uint , labelNames [ ] string ) string {
uid := assertAndroidProfile ( name + ".json" , teamID , labelNames , http . StatusOK , "" )
var wantJSON string
if teamID == 0 {
wantJSON = fmt . Sprintf ( ` { "team_id": null, "team_name": null, "profile_name": %q} ` , name )
} else {
wantJSON = fmt . Sprintf ( ` { "team_id": %d, "team_name": %q, "profile_name": %q} ` , teamID , testTeam . Name , name )
}
s . lastActivityOfTypeMatches ( fleet . ActivityTypeCreatedAndroidProfile { } . ActivityName ( ) , wantJSON , 0 )
return uid
}
2024-04-18 21:01:37 +00:00
// create a couple Apple profiles for no-team and team
noTeamAppleProfUUID := createAppleProfile ( "apple-global-profile" , "test-global-ident" , 0 , nil )
teamAppleProfUUID := createAppleProfile ( "apple-team-profile" , "test-team-ident" , testTeam . ID , nil )
// create a couple Windows profiles for no-team and team
noTeamWinProfUUID := createWindowsProfile ( "win-global-profile" , 0 , nil )
teamWinProfUUID := createWindowsProfile ( "win-team-profile" , testTeam . ID , nil )
2025-09-22 15:29:57 +00:00
// Create a couple Android profiles for no-team and team
noTeamAndroidProfUUID := createAndroidProfile ( "android-global-profile" , 0 , nil )
teamAndroidProfUUID := createAndroidProfile ( "android-team-profile" , testTeam . ID , nil )
2024-04-18 21:01:37 +00:00
// Windows profile name conflicts with Apple's for no team
2025-01-30 11:17:36 +00:00
assertWindowsProfile ( "apple-global-profile.xml" , "./Test" , 0 , nil , http . StatusConflict , SameProfileNameUploadErrorMsg )
2024-04-18 21:01:37 +00:00
// but no conflict for team 1
assertWindowsProfile ( "apple-global-profile.xml" , "./Test" , testTeam . ID , nil , http . StatusOK , "" )
// Apple profile name conflicts with Windows' for no team
2025-01-30 11:17:36 +00:00
assertAppleProfile ( "win-global-profile.mobileconfig" , "win-global-profile" , "test-global-ident-2" , 0 , nil , http . StatusConflict , SameProfileNameUploadErrorMsg )
2024-04-18 21:01:37 +00:00
// but no conflict for team 1
assertAppleProfile ( "win-global-profile.mobileconfig" , "win-global-profile" , "test-global-ident-2" , testTeam . ID , nil , http . StatusOK , "" )
// Windows profile name conflicts with Apple's for team 1
2025-01-30 11:17:36 +00:00
assertWindowsProfile ( "apple-team-profile.xml" , "./Test" , testTeam . ID , nil , http . StatusConflict , SameProfileNameUploadErrorMsg )
2024-04-18 21:01:37 +00:00
// but no conflict for no-team
assertWindowsProfile ( "apple-team-profile.xml" , "./Test" , 0 , nil , http . StatusOK , "" )
// Apple profile name conflicts with Windows' for team 1
2025-01-30 11:17:36 +00:00
assertAppleProfile ( "win-team-profile.mobileconfig" , "win-team-profile" , "test-team-ident-2" , testTeam . ID , nil , http . StatusConflict , SameProfileNameUploadErrorMsg )
2024-04-18 21:01:37 +00:00
// but no conflict for no-team
assertAppleProfile ( "win-team-profile.mobileconfig" , "win-team-profile" , "test-team-ident-2" , 0 , nil , http . StatusOK , "" )
2025-09-22 15:29:57 +00:00
// Android profile name conflicts with Apple's for no team
assertAndroidProfile ( "apple-global-profile.json" , 0 , nil , http . StatusConflict , SameProfileNameUploadErrorMsg )
2024-04-18 21:01:37 +00:00
// add some macOS declarations
createAppleDeclaration ( "apple-declaration" , "test-declaration-ident" , 0 , nil )
// identifier must be unique, it conflicts with existing declaration
assertAppleDeclaration ( "apple-declaration.json" , "test-declaration-ident" , 0 , nil , http . StatusConflict , "test-declaration-ident already exists" )
// name is pulled from filename, it conflicts with existing declaration
2025-02-07 15:53:14 +00:00
assertAppleDeclaration ( "apple-declaration.json" , "test-declaration-ident-2" , 0 , nil , http . StatusConflict , SameProfileNameUploadErrorMsg )
2024-04-18 21:01:37 +00:00
// uniqueness is checked only within team, so it's fine to have the same name and identifier in different teams
assertAppleDeclaration ( "apple-declaration.json" , "test-declaration-ident" , testTeam . ID , nil , http . StatusOK , "" )
// name is pulled from filename, it conflicts with existing macOS config profile
2025-02-07 15:53:14 +00:00
assertAppleDeclaration ( "apple-global-profile.json" , "test-declaration-ident-2" , 0 , nil , http . StatusConflict , SameProfileNameUploadErrorMsg )
2024-04-18 21:01:37 +00:00
// name is pulled from filename, it conflicts with existing macOS config profile
2025-02-07 15:53:14 +00:00
assertAppleDeclaration ( "win-global-profile.json" , "test-declaration-ident-2" , 0 , nil , http . StatusConflict , SameProfileNameUploadErrorMsg )
2025-09-22 15:29:57 +00:00
// name is pulled from filename, it conflicts with existing Android config profile
assertAppleDeclaration ( "android-global-profile.json" , "test-declaration-ident-2" , 0 , nil , http . StatusConflict , SameProfileNameUploadErrorMsg )
2024-04-18 21:01:37 +00:00
// windows profile name conflicts with existing declaration
2025-01-30 11:17:36 +00:00
assertWindowsProfile ( "apple-declaration.xml" , "./Test" , 0 , nil , http . StatusConflict , SameProfileNameUploadErrorMsg )
2024-04-18 21:01:37 +00:00
// macOS profile name conflicts with existing declaration
2025-01-30 11:17:36 +00:00
assertAppleProfile ( "apple-declaration.mobileconfig" , "apple-declaration" , "test-declaration-ident" , 0 , nil , http . StatusConflict , SameProfileNameUploadErrorMsg )
2025-09-22 15:29:57 +00:00
// Android profile name conflicts with existing declaration
assertAndroidProfile ( "apple-declaration.json" , 0 , nil , http . StatusConflict , SameProfileNameUploadErrorMsg )
2024-04-18 21:01:37 +00:00
// not an xml nor mobileconfig file
assertWindowsProfile ( "foo.txt" , "./Test" , 0 , nil , http . StatusBadRequest , "Couldn't add profile. The file should be a .mobileconfig, XML, or JSON file." )
assertAppleProfile ( "foo.txt" , "foo" , "foo-ident" , 0 , nil , http . StatusBadRequest , "Couldn't add profile. The file should be a .mobileconfig, XML, or JSON file." )
assertAppleDeclaration ( "foo.txt" , "foo-ident" , 0 , nil , http . StatusBadRequest , "Couldn't add profile. The file should be a .mobileconfig, XML, or JSON file." )
2025-09-22 15:29:57 +00:00
assertAndroidProfile ( "foo.txt" , 0 , nil , http . StatusBadRequest , "Couldn't add profile. The file should be a .mobileconfig, XML, or JSON file." )
2024-04-18 21:01:37 +00:00
// Windows-reserved LocURI
2025-03-21 14:56:50 +00:00
assertWindowsProfile ( "bitlocker.xml" , syncml . FleetBitLockerTargetLocURI , 0 , nil , http . StatusBadRequest ,
2025-03-21 19:24:52 +00:00
syncml . DiskEncryptionProfileRestrictionErrMsg )
2025-03-21 14:56:50 +00:00
assertWindowsProfile ( "updates.xml" , syncml . FleetOSUpdateTargetLocURI , testTeam . ID , nil , http . StatusBadRequest ,
"Couldn't add. Custom configuration profiles can't include Windows updates settings." )
2024-04-18 21:01:37 +00:00
// Fleet-reserved profiles
for name := range servermdm . FleetReservedProfileNames ( ) {
assertAppleProfile ( name + ".mobileconfig" , name , name + "-ident" , 0 , nil , http . StatusBadRequest , fmt . Sprintf ( ` name %s is not allowed ` , name ) )
assertAppleDeclaration ( name + ".json" , name + "-ident" , 0 , nil , http . StatusBadRequest , fmt . Sprintf ( ` name %q is not allowed ` , name ) )
2025-03-20 20:44:09 +00:00
assertWindowsProfile ( name + ".xml" , "./Test" , 0 , nil , http . StatusBadRequest , fmt . Sprintf ( ` Couldn't add. Profile name %q is not allowed. ` , name ) )
2024-04-18 21:01:37 +00:00
}
// profiles with non-existent labels
assertAppleProfile ( "apple-profile-with-labels.mobileconfig" , "apple-profile-with-labels" , "ident-with-labels" , 0 , [ ] string { "does-not-exist" } , http . StatusBadRequest , "some or all the labels provided don't exist" )
assertAppleDeclaration ( "apple-declaration-with-labels.json" , "ident-with-labels" , 0 , [ ] string { "does-not-exist" } , http . StatusBadRequest , "some or all the labels provided don't exist" )
assertWindowsProfile ( "win-profile-with-labels.xml" , "./Test" , 0 , [ ] string { "does-not-exist" } , http . StatusBadRequest , "some or all the labels provided don't exist" )
2025-09-22 15:29:57 +00:00
assertAndroidProfile ( "android-with-labels.json" , 0 , [ ] string { "does-not-exist" } , http . StatusBadRequest , "some or all the labels provided don't exist" )
2024-04-18 21:01:37 +00:00
// create a couple of labels
labelFoo := & fleet . Label { Name : "foo" , Query : "select * from foo;" }
labelFoo , err = s . ds . NewLabel ( context . Background ( ) , labelFoo )
require . NoError ( t , err )
labelBar := & fleet . Label { Name : "bar" , Query : "select * from bar;" }
labelBar , err = s . ds . NewLabel ( context . Background ( ) , labelBar )
require . NoError ( t , err )
// profiles mixing existent and non-existent labels
assertAppleProfile ( "apple-profile-with-labels.mobileconfig" , "apple-profile-with-labels" , "ident-with-labels" , 0 , [ ] string { "does-not-exist" , "foo" } , http . StatusBadRequest , "some or all the labels provided don't exist" )
assertAppleDeclaration ( "apple-declaration-with-labels.json" , "ident-with-labels" , 0 , [ ] string { "does-not-exist" , "foo" } , http . StatusBadRequest , "some or all the labels provided don't exist" )
assertWindowsProfile ( "win-profile-with-labels.xml" , "./Test" , 0 , [ ] string { "does-not-exist" , "bar" } , http . StatusBadRequest , "some or all the labels provided don't exist" )
2025-09-22 15:29:57 +00:00
assertAndroidProfile ( "android-profile-with-labels.json" , 0 , [ ] string { "does-not-exist" , "bar" } , http . StatusBadRequest , "some or all the labels provided don't exist" )
2024-04-18 21:01:37 +00:00
2024-06-25 19:26:28 +00:00
// profiles with invalid mix of labels
2024-11-05 18:12:22 +00:00
assertAppleProfile ( "apple-invalid-profile-with-labels.mobileconfig" , "apple-invalid-profile-with-labels" , "ident-with-labels" , 0 , [ ] string { "foo" , "!bar" } , http . StatusBadRequest , ` Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included. ` )
2024-11-14 14:30:32 +00:00
assertAppleProfile ( "apple-invalid-profile-with-labels.mobileconfig" , "apple-invalid-profile-with-labels" , "ident-with-labels" , 0 , [ ] string { "foo" , "~bar" } , http . StatusBadRequest , ` Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included. ` )
2024-11-05 18:12:22 +00:00
assertAppleDeclaration ( "apple-invalid-decl-with-labels.json" , "ident-decl-with-labels" , 0 , [ ] string { "foo" , "-bar" } , http . StatusBadRequest , ` Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included. ` )
2024-11-14 14:30:32 +00:00
assertAppleDeclaration ( "apple-invalid-decl-with-labels.json" , "ident-decl-with-labels" , 0 , [ ] string { "foo" , "~bar" } , http . StatusBadRequest , ` Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included. ` )
2024-11-05 18:12:22 +00:00
assertWindowsProfile ( "win-invalid-profile-with-labels.xml" , "./Test" , 0 , [ ] string { "-foo" , "!bar" } , http . StatusBadRequest , ` Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included. ` )
2024-11-14 14:30:32 +00:00
assertWindowsProfile ( "win-invalid-profile-with-labels.xml" , "./Test" , 0 , [ ] string { "-foo" , "~bar" } , http . StatusBadRequest , ` Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included. ` )
2025-09-22 15:29:57 +00:00
assertAndroidProfile ( "android-invalid-profile-with-labels.json" , 0 , [ ] string { "-foo" , "!bar" } , http . StatusBadRequest , ` Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included. ` )
assertAndroidProfile ( "android-invalid-profile-with-labels.json" , 0 , [ ] string { "-foo" , "~bar" } , http . StatusBadRequest , ` Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included. ` )
2024-06-25 19:26:28 +00:00
2024-04-18 21:01:37 +00:00
// profiles with valid labels
2024-06-25 19:26:28 +00:00
uuidAppleWithLabel := assertAppleProfile ( "apple-profile-with-labels.mobileconfig" , "apple-profile-with-labels" , "ident-with-labels" , 0 , [ ] string { "!foo" } , http . StatusOK , "" )
2024-11-14 14:30:32 +00:00
uuidAppleWithInclAnyLabel := assertAppleProfile ( "apple-profile-with-incl-any-labels.mobileconfig" , "apple-profile-with-incl-any-labels" , "ident-with-incl-any-labels" , 0 , [ ] string { "~foo" , "~bar" } , http . StatusOK , "" )
2024-04-18 21:01:37 +00:00
uuidAppleDDMWithLabel := createAppleDeclaration ( "apple-decl-with-labels" , "ident-decl-with-labels" , 0 , [ ] string { "foo" } )
2024-06-25 19:26:28 +00:00
uuidWindowsWithLabel := assertWindowsProfile ( "win-profile-with-labels.xml" , "./Test" , 0 , [ ] string { "-foo" , "-bar" } , http . StatusOK , "" )
uuidAppleDDMTeamWithLabel := createAppleDeclaration ( "apple-team-decl-with-labels" , "ident-team-decl-with-labels" , testTeam . ID , [ ] string { "-foo" } )
uuidWindowsTeamWithLabel := assertWindowsProfile ( "win-team-profile-with-labels.xml" , "./Test" , testTeam . ID , [ ] string { "foo" , "bar" } , http . StatusOK , "" )
2025-09-22 15:29:57 +00:00
uuidWindowsTeamWithInclAnyLabel := assertWindowsProfile ( "win-team-profile-with-incl-any-labels.xml" , "./Test" , testTeam . ID , [ ] string { "~foo" , "~bar" } , http . StatusOK , "" )
uuidAndroidWithLabel := assertAndroidProfile ( "android-profile-with-labels.json" , 0 , [ ] string { "-foo" , "-bar" } , http . StatusOK , "" )
uuidAndroidTeamWithLabel := assertAndroidProfile ( "android-team-profile-with-labels.json" , testTeam . ID , [ ] string { "foo" , "bar" } , http . StatusOK , "" )
uuidAndroidTeamWithInclAnyLabel := assertAndroidProfile ( "android-team-profile-with-incl-any-labels.json" , testTeam . ID , [ ] string { "~foo" , "~bar" } , http . StatusOK , "" )
2024-04-18 21:01:37 +00:00
// Windows invalid content
body , headers := generateNewProfileMultipartRequest ( t , "win.xml" , [ ] byte ( "\x00\x01\x02" ) , s . token , nil )
res := s . DoRawWithHeaders ( "POST" , "/api/latest/fleet/configuration_profiles" , body . Bytes ( ) , http . StatusBadRequest , headers )
errMsg := extractServerErrorText ( res . Body )
2025-03-20 20:44:09 +00:00
require . Contains ( t , errMsg , "Couldn't add. The file should include valid XML:" )
2024-04-18 21:01:37 +00:00
// Apple invalid mobileconfig content
body , headers = generateNewProfileMultipartRequest ( t ,
"apple.mobileconfig" , [ ] byte ( "\x00\x01\x02" ) , s . token , nil )
res = s . DoRawWithHeaders ( "POST" , "/api/latest/fleet/configuration_profiles" , body . Bytes ( ) , http . StatusBadRequest , headers )
errMsg = extractServerErrorText ( res . Body )
2025-09-25 11:50:48 +00:00
require . Contains ( t , errMsg , "Configuration profiles can't be signed. Fleet wil sign the profile for you." )
2024-04-18 21:01:37 +00:00
2025-09-22 15:29:57 +00:00
// Apple/Android invalid json declaration
2024-04-18 21:01:37 +00:00
body , headers = generateNewProfileMultipartRequest ( t ,
"apple.json" , [ ] byte ( "{" ) , s . token , nil )
res = s . DoRawWithHeaders ( "POST" , "/api/latest/fleet/configuration_profiles" , body . Bytes ( ) , http . StatusBadRequest , headers )
errMsg = extractServerErrorText ( res . Body )
2025-03-20 20:44:09 +00:00
require . Contains ( t , errMsg , "Couldn't add. The file should include valid JSON:" )
2024-04-18 21:01:37 +00:00
2025-09-22 15:29:57 +00:00
// Apple/Android cannot determine which
body , headers = generateNewProfileMultipartRequest ( t ,
"apple_or_android.json" , [ ] byte ( ` { "lower_key": true,"UpperKey": false} ` ) , s . token , nil )
res = s . DoRawWithHeaders ( "POST" , "/api/latest/fleet/configuration_profiles" , body . Bytes ( ) , http . StatusBadRequest , headers )
errMsg = extractServerErrorText ( res . Body )
require . Contains ( t , errMsg , "Keys in declaration (DDM) profile must contain only letters and start with a uppercase letter. Keys in Android profile must contain only letters and start with a lowercase letter." )
// Android invalid keys
for key , expectedErr := range fleet . AndroidForbiddenJSONKeys {
body , headers = generateNewProfileMultipartRequest ( t ,
"android.json" , [ ] byte ( fmt . Sprintf ( ` { "%s": true} ` , key ) ) , s . token , nil )
res = s . DoRawWithHeaders ( "POST" , "/api/latest/fleet/configuration_profiles" , body . Bytes ( ) , http . StatusBadRequest , headers )
errMsg = extractServerErrorText ( res . Body )
require . Contains ( t , errMsg , expectedErr )
}
2024-04-18 21:01:37 +00:00
// get the existing profiles work
expectedProfiles := [ ] fleet . MDMConfigProfilePayload {
2025-06-16 20:46:38 +00:00
{ ProfileUUID : noTeamAppleProfUUID , Platform : "darwin" , Name : "apple-global-profile" , Identifier : "test-global-ident" , TeamID : nil , Scope : string ( fleet . PayloadScopeSystem ) } ,
{ ProfileUUID : teamAppleProfUUID , Platform : "darwin" , Name : "apple-team-profile" , Identifier : "test-team-ident" , TeamID : & testTeam . ID , Scope : string ( fleet . PayloadScopeSystem ) } ,
2024-04-18 21:01:37 +00:00
{ ProfileUUID : noTeamWinProfUUID , Platform : "windows" , Name : "win-global-profile" , TeamID : nil } ,
{ ProfileUUID : teamWinProfUUID , Platform : "windows" , Name : "win-team-profile" , TeamID : & testTeam . ID } ,
2025-09-22 15:29:57 +00:00
{ ProfileUUID : noTeamAndroidProfUUID , Platform : "android" , Name : "android-global-profile" , TeamID : nil } ,
{ ProfileUUID : teamAndroidProfUUID , Platform : "android" , Name : "android-team-profile" , TeamID : & testTeam . ID } ,
2024-07-24 17:34:23 +00:00
{
ProfileUUID : uuidAppleDDMWithLabel , Platform : "darwin" , Name : "apple-decl-with-labels" , Identifier : "ident-decl-with-labels" , TeamID : nil ,
2024-06-25 19:26:28 +00:00
LabelsIncludeAll : [ ] fleet . ConfigurationProfileLabel {
{ LabelID : labelFoo . ID , LabelName : labelFoo . Name } ,
} ,
} ,
2024-07-24 17:34:23 +00:00
{
2025-06-16 20:46:38 +00:00
ProfileUUID : uuidAppleWithLabel , Platform : "darwin" , Name : "apple-profile-with-labels" , Identifier : "ident-with-labels" , TeamID : nil , Scope : string ( fleet . PayloadScopeSystem ) ,
2024-06-25 19:26:28 +00:00
LabelsIncludeAll : [ ] fleet . ConfigurationProfileLabel {
{ LabelID : labelFoo . ID , LabelName : labelFoo . Name } ,
} ,
} ,
2024-11-14 14:30:32 +00:00
{
2025-06-16 20:46:38 +00:00
ProfileUUID : uuidAppleWithInclAnyLabel , Platform : "darwin" , Name : "apple-profile-with-incl-any-labels" , Identifier : "ident-with-incl-any-labels" , TeamID : nil , Scope : string ( fleet . PayloadScopeSystem ) ,
2024-11-14 14:30:32 +00:00
LabelsIncludeAny : [ ] fleet . ConfigurationProfileLabel {
{ LabelID : labelBar . ID , LabelName : labelBar . Name } ,
{ LabelID : labelFoo . ID , LabelName : labelFoo . Name } ,
} ,
} ,
2024-07-24 17:34:23 +00:00
{
ProfileUUID : uuidWindowsWithLabel , Platform : "windows" , Name : "win-profile-with-labels" , TeamID : nil ,
2024-06-25 19:26:28 +00:00
LabelsExcludeAny : [ ] fleet . ConfigurationProfileLabel {
{ LabelID : labelBar . ID , LabelName : labelBar . Name } ,
{ LabelID : labelFoo . ID , LabelName : labelFoo . Name } ,
} ,
} ,
2024-07-24 17:34:23 +00:00
{
ProfileUUID : uuidAppleDDMTeamWithLabel , Platform : "darwin" , Name : "apple-team-decl-with-labels" , Identifier : "ident-team-decl-with-labels" , TeamID : & testTeam . ID ,
2024-06-25 19:26:28 +00:00
LabelsExcludeAny : [ ] fleet . ConfigurationProfileLabel {
{ LabelID : labelFoo . ID , LabelName : labelFoo . Name } ,
} ,
} ,
2024-07-24 17:34:23 +00:00
{
ProfileUUID : uuidWindowsTeamWithLabel , Platform : "windows" , Name : "win-team-profile-with-labels" , TeamID : & testTeam . ID ,
2024-06-25 19:26:28 +00:00
LabelsIncludeAll : [ ] fleet . ConfigurationProfileLabel {
{ LabelID : labelBar . ID , LabelName : labelBar . Name } ,
{ LabelID : labelFoo . ID , LabelName : labelFoo . Name } ,
} ,
} ,
2024-11-14 14:30:32 +00:00
{
ProfileUUID : uuidWindowsTeamWithInclAnyLabel , Platform : "windows" , Name : "win-team-profile-with-incl-any-labels" , TeamID : & testTeam . ID ,
2025-09-22 15:29:57 +00:00
LabelsIncludeAny : [ ] fleet . ConfigurationProfileLabel {
{ LabelID : labelBar . ID , LabelName : labelBar . Name } ,
{ LabelID : labelFoo . ID , LabelName : labelFoo . Name } ,
} ,
} ,
{
ProfileUUID : uuidAndroidWithLabel , Platform : "android" , Name : "android-profile-with-labels" , TeamID : nil ,
LabelsExcludeAny : [ ] fleet . ConfigurationProfileLabel {
{ LabelID : labelBar . ID , LabelName : labelBar . Name } ,
{ LabelID : labelFoo . ID , LabelName : labelFoo . Name } ,
} ,
} ,
{
ProfileUUID : uuidAndroidTeamWithLabel , Platform : "android" , Name : "android-team-profile-with-labels" , TeamID : & testTeam . ID ,
2024-11-14 14:30:32 +00:00
LabelsIncludeAll : [ ] fleet . ConfigurationProfileLabel {
{ LabelID : labelBar . ID , LabelName : labelBar . Name } ,
{ LabelID : labelFoo . ID , LabelName : labelFoo . Name } ,
} ,
} ,
2025-09-22 15:29:57 +00:00
{
ProfileUUID : uuidAndroidTeamWithInclAnyLabel , Platform : "android" , Name : "android-team-profile-with-incl-any-labels" , TeamID : & testTeam . ID ,
LabelsIncludeAny : [ ] fleet . ConfigurationProfileLabel {
{ LabelID : labelBar . ID , LabelName : labelBar . Name } ,
{ LabelID : labelFoo . ID , LabelName : labelFoo . Name } ,
} ,
} ,
2024-04-18 21:01:37 +00:00
}
for _ , prof := range expectedProfiles {
var getResp getMDMConfigProfileResponse
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , prof . ProfileUUID ) , nil , http . StatusOK , & getResp )
require . NotZero ( t , getResp . CreatedAt )
require . NotZero ( t , getResp . UploadedAt )
if getResp . Platform == "darwin" {
require . Len ( t , getResp . Checksum , 16 )
} else {
require . Empty ( t , getResp . Checksum )
}
getResp . CreatedAt , getResp . UploadedAt = time . Time { } , time . Time { }
getResp . Checksum = nil
2024-06-25 19:26:28 +00:00
// sort the labels by name
sort . Slice ( getResp . LabelsIncludeAll , func ( i , j int ) bool {
return getResp . LabelsIncludeAll [ i ] . LabelName < getResp . LabelsIncludeAll [ j ] . LabelName
} )
sort . Slice ( getResp . LabelsExcludeAny , func ( i , j int ) bool {
return getResp . LabelsExcludeAny [ i ] . LabelName < getResp . LabelsExcludeAny [ j ] . LabelName
} )
2024-11-14 14:30:32 +00:00
sort . Slice ( getResp . LabelsIncludeAny , func ( i , j int ) bool {
return getResp . LabelsIncludeAny [ i ] . LabelName < getResp . LabelsIncludeAny [ j ] . LabelName
} )
2024-04-18 21:01:37 +00:00
require . Equal ( t , prof , * getResp . MDMConfigProfilePayload )
resp := s . Do ( "GET" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , prof . ProfileUUID ) , nil , http . StatusOK , "alt" , "media" )
require . NotZero ( t , resp . ContentLength )
require . Contains ( t , resp . Header . Get ( "Content-Disposition" ) , "attachment;" )
2025-09-22 15:29:57 +00:00
if strings . HasPrefix ( prof . ProfileUUID , fleet . MDMAppleProfileUUIDPrefix ) { //nolint:gocritic // ignore ifElseChain
2024-04-18 21:01:37 +00:00
require . Contains ( t , resp . Header . Get ( "Content-Type" ) , "application/x-apple-aspen-config" )
} else if strings . HasPrefix ( prof . ProfileUUID , fleet . MDMAppleDeclarationUUIDPrefix ) {
require . Contains ( t , resp . Header . Get ( "Content-Type" ) , "application/json" )
2025-09-22 15:29:57 +00:00
} else if strings . HasPrefix ( prof . ProfileUUID , fleet . MDMAndroidProfileUUIDPrefix ) {
require . Contains ( t , resp . Header . Get ( "Content-Type" ) , "application/json" )
2024-04-18 21:01:37 +00:00
} else {
require . Contains ( t , resp . Header . Get ( "Content-Type" ) , "application/octet-stream" )
}
require . Contains ( t , resp . Header . Get ( "X-Content-Type-Options" ) , "nosniff" )
b , err := io . ReadAll ( resp . Body )
require . NoError ( t , err )
require . Equal ( t , resp . ContentLength , int64 ( len ( b ) ) )
}
var getResp getMDMConfigProfileResponse
// get an unknown Apple profile
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , "ano-such-profile" ) , nil , http . StatusNotFound , & getResp )
s . Do ( "GET" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , "ano-such-profile" ) , nil , http . StatusNotFound , "alt" , "media" )
// get an unknown Apple declaration
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , fmt . Sprintf ( "%sno-such-profile" , fleet . MDMAppleDeclarationUUIDPrefix ) ) , nil , http . StatusNotFound , & getResp )
s . Do ( "GET" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , fmt . Sprintf ( "%sno-such-profile" , fleet . MDMAppleDeclarationUUIDPrefix ) ) , nil , http . StatusNotFound , "alt" , "media" )
// get an unknown Windows profile
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , "wno-such-profile" ) , nil , http . StatusNotFound , & getResp )
s . Do ( "GET" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , "wno-such-profile" ) , nil , http . StatusNotFound , "alt" , "media" )
2025-09-22 15:29:57 +00:00
// get an unknown Android profile
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , fmt . Sprintf ( "%sno-such-profile" , fleet . MDMAndroidProfileUUIDPrefix ) ) , nil , http . StatusNotFound , & getResp )
s . Do ( "GET" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , fmt . Sprintf ( "%sno-such-profile" , fleet . MDMAndroidProfileUUIDPrefix ) ) , nil , http . StatusNotFound , "alt" , "media" )
2024-04-18 21:01:37 +00:00
var deleteResp deleteMDMConfigProfileResponse
// delete existing Apple profiles
s . DoJSON ( "DELETE" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , noTeamAppleProfUUID ) , nil , http . StatusOK , & deleteResp )
2025-12-18 21:14:32 +00:00
// turn off apple MDM
appCfg , err := s . ds . AppConfig ( ctx )
require . NoError ( t , err )
appCfg . MDM . EnabledAndConfigured = false
err = s . ds . SaveAppConfig ( ctx , appCfg )
require . NoError ( t , err )
2024-04-18 21:01:37 +00:00
s . DoJSON ( "DELETE" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , teamAppleProfUUID ) , nil , http . StatusOK , & deleteResp )
// delete non-existing Apple profile
s . DoJSON ( "DELETE" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , "ano-such-profile" ) , nil , http . StatusNotFound , & deleteResp )
// delete existing Apple declaration
s . DoJSON ( "DELETE" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , uuidAppleDDMWithLabel ) , nil , http . StatusOK , & deleteResp )
s . lastActivityOfTypeMatches (
fleet . ActivityTypeDeletedDeclarationProfile { } . ActivityName ( ) ,
` { "profile_name": "apple-decl-with-labels", "identifier": "ident-decl-with-labels", "team_id": null, "team_name": null} ` ,
0 ,
)
// delete non-existing Apple declaration
s . DoJSON ( "DELETE" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , fmt . Sprintf ( "%sno-such-profile" , fleet . MDMAppleDeclarationUUIDPrefix ) ) , nil , http . StatusNotFound , & deleteResp )
// delete existing Windows profiles
s . DoJSON ( "DELETE" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , noTeamWinProfUUID ) , nil , http . StatusOK , & deleteResp )
2025-12-18 21:14:32 +00:00
// Now disabling windows MDM
filler := struct { } { }
s . DoJSON ( "PATCH" , "/api/latest/fleet/config" , json . RawMessage ( ` { "mdm": { "windows_enabled_and_configured": false}} ` ) , http . StatusOK , & filler )
2024-04-18 21:01:37 +00:00
s . DoJSON ( "DELETE" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , teamWinProfUUID ) , nil , http . StatusOK , & deleteResp )
// delete non-existing Windows profile
s . DoJSON ( "DELETE" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , "wno-such-profile" ) , nil , http . StatusNotFound , & deleteResp )
2025-09-22 15:29:57 +00:00
// delete existing Android profiles
s . DoJSON ( "DELETE" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , noTeamAndroidProfUUID ) , nil , http . StatusOK , & deleteResp )
s . lastActivityOfTypeMatches (
fleet . ActivityTypeDeletedAndroidProfile { } . ActivityName ( ) ,
` { "profile_name": "android-global-profile", "team_id": null, "team_name": null} ` ,
0 ,
)
2025-12-18 21:14:32 +00:00
// turn off Android MDM
appCfg , err = s . ds . AppConfig ( ctx )
require . NoError ( t , err )
appCfg . MDM . AndroidEnabledAndConfigured = false
err = s . ds . SaveAppConfig ( ctx , appCfg )
require . NoError ( t , err )
2025-09-22 15:29:57 +00:00
s . DoJSON ( "DELETE" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , teamAndroidProfUUID ) , nil , http . StatusOK , & deleteResp )
s . lastActivityOfTypeMatches (
fleet . ActivityTypeDeletedAndroidProfile { } . ActivityName ( ) ,
fmt . Sprintf ( ` { "profile_name": "android-team-profile", "team_id": %d, "team_name": %q} ` , testTeam . ID , testTeam . Name ) ,
0 ,
)
// delete non-existing Android profiles
s . DoJSON ( "DELETE" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , fmt . Sprintf ( "%sno-such-profile" , fleet . MDMAndroidProfileUUIDPrefix ) ) , nil , http . StatusNotFound , & deleteResp )
2025-12-18 21:14:32 +00:00
// turn back on apple MDM
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
_ , err = q . ExecContext ( ctx , "UPDATE app_config_json SET json_value = JSON_SET(json_value, '$.mdm.enabled_and_configured', true) " )
return err
} )
2024-04-18 21:01:37 +00:00
// trying to create/delete profiles managed by Fleet fails
for p := range mobileconfig . FleetPayloadIdentifiers ( ) {
assertAppleProfile ( "foo.mobileconfig" , p , p , 0 , nil , http . StatusBadRequest , fmt . Sprintf ( "payload identifier %s is not allowed" , p ) )
// create it directly in the DB to test deletion
uid := "a" + uuid . NewString ( )
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
mc := mcBytesForTest ( p , p , uuid . New ( ) . String ( ) )
_ , err := q . ExecContext ( ctx ,
"INSERT INTO mdm_apple_configuration_profiles (profile_uuid, identifier, name, mobileconfig, checksum, team_id, uploaded_at) VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP())" ,
uid , p , p , mc , "1234" , 0 )
return err
} )
var deleteResp deleteMDMConfigProfileResponse
s . DoJSON ( "DELETE" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , uid ) , nil , http . StatusBadRequest , & deleteResp )
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
_ , err := q . ExecContext ( ctx ,
"DELETE FROM mdm_apple_configuration_profiles WHERE profile_uuid = ?" ,
uid )
return err
} )
}
// TODO: Add tests for create/delete forbidden declaration types?
// make fleet add a FileVault profile
acResp := appConfigResponse { }
s . DoJSON ( "PATCH" , "/api/latest/fleet/config" , json . RawMessage ( ` {
"mdm" : { "enable_disk_encryption" : true }
} ` ) , http . StatusOK , & acResp )
assert . True ( t , acResp . MDM . EnableDiskEncryption . Value )
profile := s . assertConfigProfilesByIdentifier ( nil , mobileconfig . FleetFileVaultPayloadIdentifier , true )
// try to delete the profile
s . DoJSON ( "DELETE" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , profile . ProfileUUID ) , nil , http . StatusBadRequest , & deleteResp )
// make fleet add a Windows OS Updates profile
acResp = appConfigResponse { }
s . DoJSON ( "PATCH" , "/api/latest/fleet/config" , json . RawMessage ( ` {
"mdm" : { "windows_updates" : { "deadline_days" : 1 , "grace_period_days" : 1 } }
} ` ) , http . StatusOK , & acResp )
profUUID := checkWindowsOSUpdatesProfile ( t , s . ds , nil , & fleet . WindowsUpdates { DeadlineDays : optjson . SetInt ( 1 ) , GracePeriodDays : optjson . SetInt ( 1 ) } )
// try to delete the profile
s . DoJSON ( "DELETE" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , profUUID ) , nil , http . StatusBadRequest , & deleteResp )
// TODO: Add tests for OS updates declaration when implemented.
}
func ( s * integrationMDMTestSuite ) TestListMDMConfigProfiles ( ) {
t := s . T ( )
ctx := context . Background ( )
// create some teams
tm1 , err := s . ds . NewTeam ( ctx , & fleet . Team { Name : "team1" } )
require . NoError ( t , err )
tm2 , err := s . ds . NewTeam ( ctx , & fleet . Team { Name : "team2" } )
require . NoError ( t , err )
tm3 , err := s . ds . NewTeam ( ctx , & fleet . Team { Name : "team3" } )
require . NoError ( t , err )
// create 5 profiles for no team and team 1, names are A, B, C ... for global and
// tA, tB, tC ... for team 1. Alternate macOS and Windows profiles.
for i := 0 ; i < 5 ; i ++ {
name := string ( 'A' + byte ( i ) )
if i % 2 == 0 {
prof , err := fleet . NewMDMAppleConfigProfile ( mcBytesForTest ( name , name + ".identifier" , name + ".uuid" ) , nil )
require . NoError ( t , err )
2025-04-30 20:03:23 +00:00
_ , err = s . ds . NewMDMAppleConfigProfile ( ctx , * prof , nil )
2024-04-18 21:01:37 +00:00
require . NoError ( t , err )
tprof , err := fleet . NewMDMAppleConfigProfile ( mcBytesForTest ( "t" + name , "t" + name + ".identifier" , "t" + name + ".uuid" ) , nil )
require . NoError ( t , err )
tprof . TeamID = & tm1 . ID
2025-04-30 20:03:23 +00:00
_ , err = s . ds . NewMDMAppleConfigProfile ( ctx , * tprof , nil )
2024-04-18 21:01:37 +00:00
require . NoError ( t , err )
} else {
2025-08-10 10:24:38 +00:00
_ , err = s . ds . NewMDMWindowsConfigProfile ( ctx , fleet . MDMWindowsConfigProfile { Name : name , SyncML : [ ] byte ( ` <Replace></Replace> ` ) } , nil )
2024-04-18 21:01:37 +00:00
require . NoError ( t , err )
2025-08-10 10:24:38 +00:00
_ , err = s . ds . NewMDMWindowsConfigProfile ( ctx , fleet . MDMWindowsConfigProfile { Name : "t" + name , TeamID : & tm1 . ID , SyncML : [ ] byte ( ` <Replace></Replace> ` ) } , nil )
2024-04-18 21:01:37 +00:00
require . NoError ( t , err )
}
}
2024-06-25 19:26:28 +00:00
lblFoo , err := s . ds . NewLabel ( ctx , & fleet . Label { Name : "foo" , Query : "select 1" } )
require . NoError ( t , err )
lblBar , err := s . ds . NewLabel ( ctx , & fleet . Label { Name : "bar" , Query : "select 1" } )
require . NoError ( t , err )
2024-11-14 14:30:32 +00:00
lblBaz , err := s . ds . NewLabel ( ctx , & fleet . Label { Name : "baz" , Query : "select 1" } )
require . NoError ( t , err )
2024-06-25 19:26:28 +00:00
2024-04-18 21:01:37 +00:00
// create a couple profiles (Win and mac) for team 2, and none for team 3
tprof , err := fleet . NewMDMAppleConfigProfile ( mcBytesForTest ( "tF" , "tF.identifier" , "tF.uuid" ) , nil )
require . NoError ( t , err )
tprof . TeamID = & tm2 . ID
2024-06-25 19:26:28 +00:00
// make tm2ProfF a "exclude-any" label-based profile
tprof . LabelsExcludeAny = [ ] fleet . ConfigurationProfileLabel {
{ LabelID : lblFoo . ID , LabelName : lblFoo . Name } ,
{ LabelID : lblBar . ID , LabelName : lblBar . Name } ,
}
2025-04-30 20:03:23 +00:00
tm2ProfF , err := s . ds . NewMDMAppleConfigProfile ( ctx , * tprof , nil )
2024-04-18 21:01:37 +00:00
require . NoError ( t , err )
// checksum is not returned by New..., so compute it manually
checkSum := md5 . Sum ( tm2ProfF . Mobileconfig ) // nolint:gosec // used only for test
tm2ProfF . Checksum = checkSum [ : ]
2024-06-25 19:26:28 +00:00
// make tm2ProfG a "include-all" label-based profile
2024-04-18 21:01:37 +00:00
tm2ProfG , err := s . ds . NewMDMWindowsConfigProfile ( ctx , fleet . MDMWindowsConfigProfile {
Name : "tG" ,
TeamID : & tm2 . ID ,
SyncML : [ ] byte ( ` <Add></Add> ` ) ,
2024-06-25 19:26:28 +00:00
LabelsIncludeAll : [ ] fleet . ConfigurationProfileLabel {
2024-04-18 21:01:37 +00:00
{ LabelID : lblFoo . ID , LabelName : lblFoo . Name } ,
{ LabelID : lblBar . ID , LabelName : lblBar . Name } ,
} ,
2025-08-10 10:24:38 +00:00
} , nil )
2024-04-18 21:01:37 +00:00
require . NoError ( t , err )
2024-11-14 14:30:32 +00:00
// make tm2ProfH a "include-any" label-based profile
tm2ProfH , err := s . ds . NewMDMWindowsConfigProfile ( ctx , fleet . MDMWindowsConfigProfile {
Name : "tH" ,
TeamID : & tm2 . ID ,
SyncML : [ ] byte ( ` <Add></Add> ` ) ,
LabelsIncludeAny : [ ] fleet . ConfigurationProfileLabel {
{ LabelID : lblBar . ID , LabelName : lblBar . Name } ,
{ LabelID : lblBaz . ID , LabelName : lblBaz . Name } ,
} ,
2025-08-10 10:24:38 +00:00
} , nil )
2024-11-14 14:30:32 +00:00
require . NoError ( t , err )
2024-04-18 21:01:37 +00:00
// break lblFoo by deleting it
2025-12-30 03:28:45 +00:00
require . NoError ( t , s . ds . DeleteLabel ( ctx , lblFoo . Name , fleet . TeamFilter { User : & fleet . User { GlobalRole : ptr . String ( fleet . RoleAdmin ) } } ) )
2024-04-18 21:01:37 +00:00
// test that all fields are correctly returned with team 2
var listResp listMDMConfigProfilesResponse
s . DoJSON ( "GET" , "/api/latest/fleet/configuration_profiles" , nil , http . StatusOK , & listResp , "team_id" , fmt . Sprint ( tm2 . ID ) )
2024-11-14 14:30:32 +00:00
require . Len ( t , listResp . Profiles , 3 )
2024-04-18 21:01:37 +00:00
require . NotZero ( t , listResp . Profiles [ 0 ] . CreatedAt )
require . NotZero ( t , listResp . Profiles [ 0 ] . UploadedAt )
require . NotZero ( t , listResp . Profiles [ 1 ] . CreatedAt )
require . NotZero ( t , listResp . Profiles [ 1 ] . UploadedAt )
listResp . Profiles [ 0 ] . CreatedAt , listResp . Profiles [ 0 ] . UploadedAt = time . Time { } , time . Time { }
listResp . Profiles [ 1 ] . CreatedAt , listResp . Profiles [ 1 ] . UploadedAt = time . Time { } , time . Time { }
2024-11-14 14:30:32 +00:00
listResp . Profiles [ 2 ] . CreatedAt , listResp . Profiles [ 2 ] . UploadedAt = time . Time { } , time . Time { }
2024-04-18 21:01:37 +00:00
require . Equal ( t , & fleet . MDMConfigProfilePayload {
ProfileUUID : tm2ProfF . ProfileUUID ,
TeamID : tm2ProfF . TeamID ,
Name : tm2ProfF . Name ,
Platform : "darwin" ,
Identifier : tm2ProfF . Identifier ,
Checksum : tm2ProfF . Checksum ,
2025-06-16 20:46:38 +00:00
Scope : string ( fleet . PayloadScopeSystem ) ,
2024-06-25 19:26:28 +00:00
// labels are ordered by name
LabelsExcludeAny : [ ] fleet . ConfigurationProfileLabel {
{ LabelID : lblBar . ID , LabelName : lblBar . Name } ,
{ LabelID : 0 , LabelName : lblFoo . Name , Broken : true } ,
} ,
2024-04-18 21:01:37 +00:00
} , listResp . Profiles [ 0 ] )
require . Equal ( t , & fleet . MDMConfigProfilePayload {
ProfileUUID : tm2ProfG . ProfileUUID ,
TeamID : tm2ProfG . TeamID ,
Name : tm2ProfG . Name ,
Platform : "windows" ,
// labels are ordered by name
2024-06-25 19:26:28 +00:00
LabelsIncludeAll : [ ] fleet . ConfigurationProfileLabel {
2024-04-18 21:01:37 +00:00
{ LabelID : lblBar . ID , LabelName : lblBar . Name } ,
{ LabelID : 0 , LabelName : lblFoo . Name , Broken : true } ,
} ,
} , listResp . Profiles [ 1 ] )
2024-11-14 14:30:32 +00:00
require . Equal ( t , & fleet . MDMConfigProfilePayload {
ProfileUUID : tm2ProfH . ProfileUUID ,
TeamID : tm2ProfH . TeamID ,
Name : tm2ProfH . Name ,
Platform : "windows" ,
// labels are ordered by name
LabelsIncludeAny : [ ] fleet . ConfigurationProfileLabel {
{ LabelID : lblBar . ID , LabelName : lblBar . Name } ,
{ LabelID : lblBaz . ID , LabelName : lblBaz . Name } ,
} ,
} , listResp . Profiles [ 2 ] )
2024-04-18 21:01:37 +00:00
2024-06-25 19:26:28 +00:00
// get the specific include-all label-based profile returns the information
2024-04-18 21:01:37 +00:00
var getProfResp getMDMConfigProfileResponse
s . DoJSON ( "GET" , "/api/latest/fleet/mdm/profiles/" + tm2ProfG . ProfileUUID , nil , http . StatusOK , & getProfResp )
getProfResp . CreatedAt , getProfResp . UploadedAt = time . Time { } , time . Time { }
require . Equal ( t , & fleet . MDMConfigProfilePayload {
ProfileUUID : tm2ProfG . ProfileUUID ,
TeamID : tm2ProfG . TeamID ,
Name : tm2ProfG . Name ,
Platform : "windows" ,
// labels are ordered by name
2024-06-25 19:26:28 +00:00
LabelsIncludeAll : [ ] fleet . ConfigurationProfileLabel {
2024-04-18 21:01:37 +00:00
{ LabelID : lblBar . ID , LabelName : lblBar . Name } ,
{ LabelID : 0 , LabelName : lblFoo . Name , Broken : true } ,
} ,
} , getProfResp . MDMConfigProfilePayload )
2024-06-25 19:26:28 +00:00
// get the specific exclude-any label-based profile returns the information
2024-04-18 21:01:37 +00:00
getProfResp = getMDMConfigProfileResponse { }
s . DoJSON ( "GET" , "/api/latest/fleet/mdm/profiles/" + tm2ProfF . ProfileUUID , nil , http . StatusOK , & getProfResp )
getProfResp . CreatedAt , getProfResp . UploadedAt = time . Time { } , time . Time { }
require . Equal ( t , & fleet . MDMConfigProfilePayload {
ProfileUUID : tm2ProfF . ProfileUUID ,
TeamID : tm2ProfF . TeamID ,
Name : tm2ProfF . Name ,
Platform : "darwin" ,
Identifier : tm2ProfF . Identifier ,
Checksum : tm2ProfF . Checksum ,
2025-06-16 20:46:38 +00:00
Scope : string ( fleet . PayloadScopeSystem ) ,
2024-06-25 19:26:28 +00:00
// labels are ordered by name
LabelsExcludeAny : [ ] fleet . ConfigurationProfileLabel {
{ LabelID : lblBar . ID , LabelName : lblBar . Name } ,
{ LabelID : 0 , LabelName : lblFoo . Name , Broken : true } ,
} ,
2024-04-18 21:01:37 +00:00
} , getProfResp . MDMConfigProfilePayload )
2024-11-14 14:30:32 +00:00
// get the specific include-any label-based profile returns the information
getProfResp = getMDMConfigProfileResponse { }
s . DoJSON ( "GET" , "/api/latest/fleet/mdm/profiles/" + tm2ProfH . ProfileUUID , nil , http . StatusOK , & getProfResp )
getProfResp . CreatedAt , getProfResp . UploadedAt = time . Time { } , time . Time { }
require . Equal ( t , & fleet . MDMConfigProfilePayload {
ProfileUUID : tm2ProfH . ProfileUUID ,
TeamID : tm2ProfH . TeamID ,
Name : tm2ProfH . Name ,
Platform : "windows" ,
// labels are ordered by name
LabelsIncludeAny : [ ] fleet . ConfigurationProfileLabel {
{ LabelID : lblBar . ID , LabelName : lblBar . Name } ,
{ LabelID : lblBaz . ID , LabelName : lblBaz . Name } ,
} ,
} , getProfResp . MDMConfigProfilePayload )
2024-04-18 21:01:37 +00:00
// list for a non-existing team returns 404
s . DoJSON ( "GET" , "/api/latest/fleet/configuration_profiles" , nil , http . StatusNotFound , & listResp , "team_id" , "99999" )
cases := [ ] struct {
queries [ ] string // alternate query name and value
teamID * uint
wantNames [ ] string
wantMeta * fleet . PaginationMetadata
} {
{
wantNames : [ ] string { "A" , "B" , "C" , "D" , "E" } ,
wantMeta : & fleet . PaginationMetadata { HasNextResults : false , HasPreviousResults : false } ,
} ,
{
queries : [ ] string { "per_page" , "2" } ,
wantNames : [ ] string { "A" , "B" } ,
wantMeta : & fleet . PaginationMetadata { HasNextResults : true , HasPreviousResults : false } ,
} ,
{
queries : [ ] string { "per_page" , "2" , "page" , "1" } ,
wantNames : [ ] string { "C" , "D" } ,
wantMeta : & fleet . PaginationMetadata { HasNextResults : true , HasPreviousResults : true } ,
} ,
{
queries : [ ] string { "per_page" , "2" , "page" , "2" } ,
wantNames : [ ] string { "E" } ,
wantMeta : & fleet . PaginationMetadata { HasNextResults : false , HasPreviousResults : true } ,
} ,
{
queries : [ ] string { "per_page" , "3" } ,
teamID : & tm1 . ID ,
wantNames : [ ] string { "tA" , "tB" , "tC" } ,
wantMeta : & fleet . PaginationMetadata { HasNextResults : true , HasPreviousResults : false } ,
} ,
{
queries : [ ] string { "per_page" , "3" , "page" , "1" } ,
teamID : & tm1 . ID ,
wantNames : [ ] string { "tD" , "tE" } ,
wantMeta : & fleet . PaginationMetadata { HasNextResults : false , HasPreviousResults : true } ,
} ,
{
queries : [ ] string { "per_page" , "3" , "page" , "2" } ,
teamID : & tm1 . ID ,
wantNames : nil ,
wantMeta : & fleet . PaginationMetadata { HasNextResults : false , HasPreviousResults : true } ,
} ,
{
queries : [ ] string { "per_page" , "3" } ,
teamID : & tm2 . ID ,
2024-11-14 14:30:32 +00:00
wantNames : [ ] string { "tF" , "tG" , "tH" } ,
2024-04-18 21:01:37 +00:00
wantMeta : & fleet . PaginationMetadata { HasNextResults : false , HasPreviousResults : false } ,
} ,
{
queries : [ ] string { "per_page" , "2" } ,
teamID : & tm3 . ID ,
wantNames : nil ,
wantMeta : & fleet . PaginationMetadata { HasNextResults : false , HasPreviousResults : false } ,
} ,
}
for _ , c := range cases {
t . Run ( fmt . Sprintf ( "%v: %#v" , c . teamID , c . queries ) , func ( t * testing . T ) {
var listResp listMDMConfigProfilesResponse
queryArgs := c . queries
if c . teamID != nil {
queryArgs = append ( queryArgs , "team_id" , fmt . Sprint ( * c . teamID ) )
}
s . DoJSON ( "GET" , "/api/latest/fleet/configuration_profiles" , nil , http . StatusOK , & listResp , queryArgs ... )
require . Equal ( t , len ( c . wantNames ) , len ( listResp . Profiles ) )
require . Equal ( t , c . wantMeta , listResp . Meta )
var gotNames [ ] string
if len ( listResp . Profiles ) > 0 {
gotNames = make ( [ ] string , len ( listResp . Profiles ) )
for i , p := range listResp . Profiles {
gotNames [ i ] = p . Name
if p . Name == "tG" {
2024-06-25 19:26:28 +00:00
require . Len ( t , p . LabelsIncludeAll , 2 )
} else {
require . Nil ( t , p . LabelsIncludeAll )
}
if p . Name == "tF" {
require . Len ( t , p . LabelsExcludeAny , 2 )
2024-04-18 21:01:37 +00:00
} else {
2024-06-25 19:26:28 +00:00
require . Nil ( t , p . LabelsExcludeAny )
2024-04-18 21:01:37 +00:00
}
if c . teamID == nil {
// we set it to 0 for global
require . NotNil ( t , p . TeamID )
require . Zero ( t , * p . TeamID )
} else {
require . NotNil ( t , p . TeamID )
require . Equal ( t , * c . teamID , * p . TeamID )
}
require . NotEmpty ( t , p . Platform )
}
}
require . Equal ( t , c . wantNames , gotNames )
} )
}
}
func ( s * integrationMDMTestSuite ) TestWindowsProfileManagement ( ) {
t := s . T ( )
ctx := context . Background ( )
err := s . ds . ApplyEnrollSecrets ( ctx , nil , [ ] * fleet . EnrollSecret { { Secret : t . Name ( ) } } )
require . NoError ( t , err )
globalProfiles := [ ] string {
mysql . InsertWindowsProfileForTest ( t , s . ds , 0 ) ,
mysql . InsertWindowsProfileForTest ( t , s . ds , 0 ) ,
mysql . InsertWindowsProfileForTest ( t , s . ds , 0 ) ,
}
// create a new team
tm , err := s . ds . NewTeam ( ctx , & fleet . Team { Name : "batch_set_mdm_profiles" } )
require . NoError ( t , err )
teamProfiles := [ ] string {
mysql . InsertWindowsProfileForTest ( t , s . ds , tm . ID ) ,
mysql . InsertWindowsProfileForTest ( t , s . ds , tm . ID ) ,
}
// create a non-Windows host
_ , err = s . ds . NewHost ( context . Background ( ) , & fleet . Host {
ID : 1 ,
OsqueryHostID : ptr . String ( "non-windows-host" ) ,
NodeKey : ptr . String ( "non-windows-host" ) ,
UUID : uuid . New ( ) . String ( ) ,
Hostname : fmt . Sprintf ( "%sfoo.local.non.windows" , t . Name ( ) ) ,
Platform : "darwin" ,
} )
require . NoError ( t , err )
// create a Windows host that's not enrolled into MDM
_ , err = s . ds . NewHost ( context . Background ( ) , & fleet . Host {
ID : 2 ,
OsqueryHostID : ptr . String ( "not-mdm-enrolled" ) ,
NodeKey : ptr . String ( "not-mdm-enrolled" ) ,
UUID : uuid . New ( ) . String ( ) ,
Hostname : fmt . Sprintf ( "%sfoo.local.not.enrolled" , t . Name ( ) ) ,
Platform : "windows" ,
} )
require . NoError ( t , err )
verifyHostProfileStatus := func ( cmds [ ] fleet . ProtoCmdOperation , wantStatus string ) {
for _ , cmd := range cmds {
var gotProfile struct {
Status string ` db:"status" `
Retries int ` db:"retries" `
}
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
stmt := `
SELECT COALESCE ( status , ' pending ' ) as status , retries
FROM host_mdm_windows_profiles
WHERE command_uuid = ? `
return sqlx . GetContext ( context . Background ( ) , q , & gotProfile , stmt , cmd . Cmd . CmdID . Value )
} )
wantDeliveryStatus := fleet . WindowsResponseToDeliveryStatus ( wantStatus )
if gotProfile . Retries <= servermdm . MaxProfileRetries && wantDeliveryStatus == fleet . MDMDeliveryFailed {
require . EqualValues ( t , "pending" , gotProfile . Status , "command_uuid" , cmd . Cmd . CmdID . Value )
} else {
require . EqualValues ( t , wantDeliveryStatus , gotProfile . Status , "command_uuid" , cmd . Cmd . CmdID . Value )
}
}
}
verifyProfiles := func ( device * mdmtest . TestWindowsMDMClient , n int , fail bool ) {
mdmResponseStatus := syncml . CmdStatusOK
if fail {
mdmResponseStatus = syncml . CmdStatusAtomicFailed
}
s . awaitTriggerProfileSchedule ( t )
cmds , err := device . StartManagementSession ( )
require . NoError ( t , err )
// 2 Status + n profiles
require . Len ( t , cmds , n + 2 )
var atomicCmds [ ] fleet . ProtoCmdOperation
msgID , err := device . GetCurrentMsgID ( )
require . NoError ( t , err )
for _ , c := range cmds {
cmdID := c . Cmd . CmdID
status := syncml . CmdStatusOK
if c . Verb == "Atomic" {
atomicCmds = append ( atomicCmds , c )
status = mdmResponseStatus
require . NotEmpty ( t , c . Cmd . ReplaceCommands )
for _ , rc := range c . Cmd . ReplaceCommands {
require . NotEmpty ( t , rc . CmdID )
}
}
device . AppendResponse ( fleet . SyncMLCmd {
XMLName : xml . Name { Local : fleet . CmdStatus } ,
MsgRef : & msgID ,
CmdRef : & cmdID . Value ,
Cmd : ptr . String ( c . Verb ) ,
Data : & status ,
Items : nil ,
CmdID : fleet . CmdID { Value : uuid . NewString ( ) } ,
} )
}
// TODO: verify profile contents as well
require . Len ( t , atomicCmds , n )
// before we send the response, commands should be "pending"
verifyHostProfileStatus ( atomicCmds , "" )
cmds , err = device . SendResponse ( )
require . NoError ( t , err )
// the ack of the message should be the only returned command
require . Len ( t , cmds , 1 )
// verify that we updated status in the db
verifyHostProfileStatus ( atomicCmds , mdmResponseStatus )
}
checkHostsProfilesMatch := func ( host * fleet . Host , wantUUIDs [ ] string ) {
var gotUUIDs [ ] string
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
stmt := ` SELECT profile_uuid FROM host_mdm_windows_profiles WHERE host_uuid = ? `
return sqlx . SelectContext ( context . Background ( ) , q , & gotUUIDs , stmt , host . UUID )
} )
require . ElementsMatch ( t , wantUUIDs , gotUUIDs )
}
checkHostDetails := func ( t * testing . T , host * fleet . Host , wantProfs [ ] string , wantStatus fleet . MDMDeliveryStatus ) {
var gotHostResp getHostResponse
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/v1/fleet/hosts/%d" , host . ID ) , nil , http . StatusOK , & gotHostResp )
require . NotNil ( t , gotHostResp . Host . MDM . Profiles )
var gotProfs [ ] string
require . Len ( t , * gotHostResp . Host . MDM . Profiles , len ( wantProfs ) )
for _ , p := range * gotHostResp . Host . MDM . Profiles {
gotProfs = append ( gotProfs , strings . Replace ( p . Name , "name-" , "" , 1 ) )
require . NotNil ( t , p . Status )
2025-12-13 19:58:35 +00:00
require . EqualValues ( t , wantStatus , * p . Status , "profile" , p . Name )
2024-04-18 21:01:37 +00:00
require . Equal ( t , "windows" , p . Platform )
// Fleet reserved profiles (e.g., OS updates) should be screened from the host details response
require . NotContains ( t , servermdm . ListFleetReservedWindowsProfileNames ( ) , p . Name )
}
require . ElementsMatch ( t , wantProfs , gotProfs )
}
checkHostsFilteredByOSSettingsStatus := func ( t * testing . T , wantHosts [ ] string , wantStatus fleet . MDMDeliveryStatus , teamID * uint , labels ... * fleet . Label ) {
var teamFilter string
if teamID != nil {
teamFilter = fmt . Sprintf ( "&team_id=%d" , * teamID )
}
var gotHostsResp listHostsResponse
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/v1/fleet/hosts?os_settings=%s%s" , wantStatus , teamFilter ) , nil , http . StatusOK , & gotHostsResp )
require . NotNil ( t , gotHostsResp . Hosts )
var gotHosts [ ] string
for _ , h := range gotHostsResp . Hosts {
gotHosts = append ( gotHosts , h . Hostname )
}
require . ElementsMatch ( t , wantHosts , gotHosts )
var countHostsResp countHostsResponse
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/v1/fleet/hosts/count?os_settings=%s%s" , wantStatus , teamFilter ) , nil , http . StatusOK , & countHostsResp )
require . Equal ( t , len ( wantHosts ) , countHostsResp . Count )
for _ , l := range labels {
gotHostsResp = listHostsResponse { }
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/v1/fleet/labels/%d/hosts?os_settings=%s%s" , l . ID , wantStatus , teamFilter ) , nil , http . StatusOK , & gotHostsResp )
require . NotNil ( t , gotHostsResp . Hosts )
gotHosts = [ ] string { }
for _ , h := range gotHostsResp . Hosts {
gotHosts = append ( gotHosts , h . Hostname )
}
require . ElementsMatch ( t , wantHosts , gotHosts , "label" , l . Name )
countHostsResp = countHostsResponse { }
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/v1/fleet/hosts/count?label_id=%d&os_settings=%s%s" , l . ID , wantStatus , teamFilter ) , nil , http . StatusOK , & countHostsResp )
}
}
getProfileUUID := func ( t * testing . T , profName string , teamID * uint ) string {
var profUUID string
mysql . ExecAdhocSQL ( t , s . ds , func ( tx sqlx . ExtContext ) error {
var globalOrTeamID uint
if teamID != nil {
globalOrTeamID = * teamID
}
return sqlx . GetContext ( ctx , tx , & profUUID , ` SELECT profile_uuid FROM mdm_windows_configuration_profiles WHERE team_id = ? AND name = ? ` , globalOrTeamID , profName )
} )
require . NotNil ( t , profUUID )
return profUUID
}
checkHostProfileStatus := func ( t * testing . T , hostUUID string , profUUID string , wantStatus fleet . MDMDeliveryStatus ) {
var gotStatus fleet . MDMDeliveryStatus
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
stmt := ` SELECT status FROM host_mdm_windows_profiles WHERE host_uuid = ? AND profile_uuid = ? `
err := sqlx . GetContext ( context . Background ( ) , q , & gotStatus , stmt , hostUUID , profUUID )
return err
} )
require . Equal ( t , wantStatus , gotStatus )
}
// Create a host and then enroll to MDM.
host , mdmDevice := createWindowsHostThenEnrollMDM ( s . ds , s . server . URL , t )
// trigger a profile sync
verifyProfiles ( mdmDevice , 3 , false )
checkHostsProfilesMatch ( host , globalProfiles )
checkHostDetails ( t , host , globalProfiles , fleet . MDMDeliveryVerifying )
2025-12-10 17:59:34 +00:00
// can't resend windows configuration profiles as admin or from device endpoint while verifying
res := s . DoRaw ( "POST" , fmt . Sprintf ( "/api/latest/fleet/hosts/%d/configuration_profiles/%s/resend" , host . ID , globalProfiles [ 0 ] ) , nil , http . StatusConflict )
2024-04-18 21:01:37 +00:00
errMsg := extractServerErrorText ( res . Body )
2025-12-10 17:59:34 +00:00
require . Contains ( t , errMsg , "Configuration profiles with “pending” or “verifying” status can’ t be resent" )
2025-09-16 07:25:02 +00:00
deviceToken := "windows-device-token"
createDeviceTokenForHost ( t , s . ds , host . ID , deviceToken )
2025-12-10 17:59:34 +00:00
res = s . DoRawNoAuth ( "POST" , fmt . Sprintf ( "/api/latest/fleet/device/%s/configuration_profiles/%s/resend" , deviceToken , globalProfiles [ 0 ] ) , nil , http . StatusConflict )
2025-09-16 07:25:02 +00:00
errMsg = extractServerErrorText ( res . Body )
2025-12-10 17:59:34 +00:00
require . Contains ( t , errMsg , "Configuration profiles with “pending” or “verifying” status can’ t be resent" )
2024-04-18 21:01:37 +00:00
// create new label that includes host
label := & fleet . Label {
Name : t . Name ( ) + "foo" ,
Query : "select * from foo;" ,
}
label , err = s . ds . NewLabel ( context . Background ( ) , label )
require . NoError ( t , err )
require . NoError ( t , s . ds . RecordLabelQueryExecutions ( ctx , host , map [ uint ] * bool { label . ID : ptr . Bool ( true ) } , time . Now ( ) , false ) )
// simulate osquery reporting host mdm details (host_mdm.enrolled = 1 is condition for
// hosts filtering by os settings status and generating mdm profiles summaries)
2025-07-22 21:24:19 +00:00
require . NoError ( t , s . ds . SetOrUpdateMDMData ( ctx , host . ID , false , true , s . server . URL , false , fleet . WellKnownMDMFleet , "" , false ) )
2024-04-18 21:01:37 +00:00
checkHostsFilteredByOSSettingsStatus ( t , [ ] string { host . Hostname } , fleet . MDMDeliveryVerifying , nil , label )
s . checkMDMProfilesSummaries ( t , nil , fleet . MDMProfilesSummary {
Verifying : 1 ,
} , nil )
// another sync shouldn't return profiles
verifyProfiles ( mdmDevice , 0 , false )
// make fleet add a Windows OS Updates profile
acResp := appConfigResponse { }
s . DoJSON ( "PATCH" , "/api/latest/fleet/config" , json . RawMessage ( ` { "mdm": { "windows_updates": { "deadline_days": 1, "grace_period_days": 1} }} ` ) , http . StatusOK , & acResp )
osUpdatesProf := getProfileUUID ( t , servermdm . FleetWindowsOSUpdatesProfileName , nil )
// os updates is sent via a profiles commands
verifyProfiles ( mdmDevice , 1 , false )
checkHostsProfilesMatch ( host , append ( globalProfiles , osUpdatesProf ) )
// but is hidden from host details response
checkHostDetails ( t , host , globalProfiles , fleet . MDMDeliveryVerifying )
// os updates profile status doesn't matter for filtered hosts results or summaries
checkHostProfileStatus ( t , host . UUID , osUpdatesProf , fleet . MDMDeliveryVerifying )
checkHostsFilteredByOSSettingsStatus ( t , [ ] string { host . Hostname } , fleet . MDMDeliveryVerifying , nil , label )
s . checkMDMProfilesSummaries ( t , nil , fleet . MDMProfilesSummary {
Verifying : 1 ,
} , nil )
// force os updates profile to failed, doesn't impact filtered hosts results or summaries
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
stmt := ` UPDATE host_mdm_windows_profiles SET status = 'failed' WHERE profile_uuid = ? `
_ , err := q . ExecContext ( context . Background ( ) , stmt , osUpdatesProf )
return err
} )
checkHostProfileStatus ( t , host . UUID , osUpdatesProf , fleet . MDMDeliveryFailed )
checkHostsFilteredByOSSettingsStatus ( t , [ ] string { host . Hostname } , fleet . MDMDeliveryVerifying , nil , label )
s . checkMDMProfilesSummaries ( t , nil , fleet . MDMProfilesSummary {
Verifying : 1 ,
} , nil )
// force another profile to failed, does impact filtered hosts results and summaries
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
stmt := ` UPDATE host_mdm_windows_profiles SET status = 'failed' WHERE profile_uuid = ? `
_ , err := q . ExecContext ( context . Background ( ) , stmt , globalProfiles [ 0 ] )
return err
} )
checkHostProfileStatus ( t , host . UUID , globalProfiles [ 0 ] , fleet . MDMDeliveryFailed )
checkHostsFilteredByOSSettingsStatus ( t , [ ] string { } , fleet . MDMDeliveryVerifying , nil , label ) // expect no hosts
checkHostsFilteredByOSSettingsStatus ( t , [ ] string { host . Hostname } , fleet . MDMDeliveryFailed , nil , label ) // expect host
s . checkMDMProfilesSummaries ( t , nil , fleet . MDMProfilesSummary {
Failed : 1 ,
Verifying : 0 ,
} , nil )
// add the host to a team
2025-07-17 14:20:49 +00:00
err = s . ds . AddHostsToTeam ( ctx , fleet . NewAddHostsToTeamParams ( & tm . ID , [ ] uint { host . ID } ) )
2024-04-18 21:01:37 +00:00
require . NoError ( t , err )
// trigger a profile sync, device gets the team profile
verifyProfiles ( mdmDevice , 2 , false )
checkHostsProfilesMatch ( host , teamProfiles )
checkHostDetails ( t , host , teamProfiles , fleet . MDMDeliveryVerifying )
// set new team profiles (delete + addition)
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
stmt := ` DELETE FROM mdm_windows_configuration_profiles WHERE profile_uuid = ? `
_ , err := q . ExecContext ( context . Background ( ) , stmt , teamProfiles [ 1 ] )
return err
} )
teamProfiles = [ ] string {
teamProfiles [ 0 ] ,
mysql . InsertWindowsProfileForTest ( t , s . ds , tm . ID ) ,
}
// trigger a profile sync, device gets the team profile
verifyProfiles ( mdmDevice , 1 , false )
// check that we deleted the old profile in the DB
checkHostsProfilesMatch ( host , teamProfiles )
checkHostDetails ( t , host , teamProfiles , fleet . MDMDeliveryVerifying )
// another sync shouldn't return profiles
verifyProfiles ( mdmDevice , 0 , false )
// set new team profiles (delete + addition)
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
stmt := ` DELETE FROM mdm_windows_configuration_profiles WHERE profile_uuid = ? `
_ , err := q . ExecContext ( context . Background ( ) , stmt , teamProfiles [ 1 ] )
return err
} )
teamProfiles = [ ] string {
teamProfiles [ 0 ] ,
mysql . InsertWindowsProfileForTest ( t , s . ds , tm . ID ) ,
}
// trigger a profile sync, this time fail the delivery
verifyProfiles ( mdmDevice , 1 , true )
// check that we deleted the old profile in the DB
checkHostsProfilesMatch ( host , teamProfiles )
// a second sync gets the profile again, because of delivery retries.
// Succeed that one
verifyProfiles ( mdmDevice , 1 , false )
// another sync shouldn't return profiles
verifyProfiles ( mdmDevice , 0 , false )
// make fleet add a Windows OS Updates profile
tmResp := teamResponse { }
s . DoJSON ( "PATCH" , fmt . Sprintf ( "/api/latest/fleet/teams/%d" , tm . ID ) , json . RawMessage ( ` { "mdm": { "windows_updates": { "deadline_days": 1, "grace_period_days": 1} }} ` ) , http . StatusOK , & tmResp )
osUpdatesProf = getProfileUUID ( t , servermdm . FleetWindowsOSUpdatesProfileName , & tm . ID )
// os updates is sent via a profiles commands
verifyProfiles ( mdmDevice , 1 , false )
checkHostsProfilesMatch ( host , append ( teamProfiles , osUpdatesProf ) )
// but is hidden from host details response
checkHostDetails ( t , host , teamProfiles , fleet . MDMDeliveryVerifying )
// os updates profile status doesn't matter for filtered hosts results or summaries
checkHostProfileStatus ( t , host . UUID , osUpdatesProf , fleet . MDMDeliveryVerifying )
checkHostsFilteredByOSSettingsStatus ( t , [ ] string { host . Hostname } , fleet . MDMDeliveryVerifying , & tm . ID , label )
s . checkMDMProfilesSummaries ( t , & tm . ID , fleet . MDMProfilesSummary {
Verifying : 1 ,
} , nil )
// force os updates profile to failed, doesn't impact filtered hosts results or summaries
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
stmt := ` UPDATE host_mdm_windows_profiles SET status = 'failed' WHERE profile_uuid = ? `
_ , err := q . ExecContext ( context . Background ( ) , stmt , osUpdatesProf )
return err
} )
checkHostProfileStatus ( t , host . UUID , osUpdatesProf , fleet . MDMDeliveryFailed )
checkHostsFilteredByOSSettingsStatus ( t , [ ] string { host . Hostname } , fleet . MDMDeliveryVerifying , & tm . ID , label )
s . checkMDMProfilesSummaries ( t , & tm . ID , fleet . MDMProfilesSummary {
Verifying : 1 ,
} , nil )
// force another profile to failed, does impact filtered hosts results and summaries
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
stmt := ` UPDATE host_mdm_windows_profiles SET status = 'failed' WHERE profile_uuid = ? `
_ , err := q . ExecContext ( context . Background ( ) , stmt , teamProfiles [ 0 ] )
return err
} )
checkHostProfileStatus ( t , host . UUID , teamProfiles [ 0 ] , fleet . MDMDeliveryFailed )
checkHostsFilteredByOSSettingsStatus ( t , [ ] string { } , fleet . MDMDeliveryVerifying , & tm . ID , label ) // expect no hosts
checkHostsFilteredByOSSettingsStatus ( t , [ ] string { host . Hostname } , fleet . MDMDeliveryFailed , & tm . ID , label ) // expect host
s . checkMDMProfilesSummaries ( t , & tm . ID , fleet . MDMProfilesSummary {
Failed : 1 ,
Verifying : 0 ,
} , nil )
2025-12-10 17:59:34 +00:00
// Resend the failed profile. Should succeed
s . DoRaw ( "POST" , fmt . Sprintf ( "/api/latest/fleet/hosts/%d/configuration_profiles/%s/resend" , host . ID , teamProfiles [ 0 ] ) , nil , http . StatusAccepted )
s . checkMDMProfilesSummaries ( t , & tm . ID , fleet . MDMProfilesSummary {
Pending : 1 ,
} , nil )
// Try resending, should fail since it's pending
res = s . DoRaw ( "POST" , fmt . Sprintf ( "/api/latest/fleet/hosts/%d/configuration_profiles/%s/resend" , host . ID , teamProfiles [ 0 ] ) , nil , http . StatusConflict )
errMsg = extractServerErrorText ( res . Body )
require . Contains ( t , errMsg , "Configuration profiles with “pending” or “verifying” status can’ t be resent" )
// Trigger a profile sync, device gets the resent profile
verifyProfiles ( mdmDevice , 1 , false )
// update to verifying - should not allow resending
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
stmt := ` UPDATE host_mdm_windows_profiles SET status = 'verifying' WHERE profile_uuid = ? `
_ , err := q . ExecContext ( context . Background ( ) , stmt , teamProfiles [ 0 ] )
return err
} )
res = s . DoRaw ( "POST" , fmt . Sprintf ( "/api/latest/fleet/hosts/%d/configuration_profiles/%s/resend" , host . ID , teamProfiles [ 0 ] ) , nil , http . StatusConflict )
errMsg = extractServerErrorText ( res . Body )
require . Contains ( t , errMsg , "Configuration profiles with “pending” or “verifying” status can’ t be resent" )
// trigger a profile sync, device doesn't get the profile since resend was not allowed
verifyProfiles ( mdmDevice , 0 , false )
// Update to verified, resending allowed again
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
stmt := ` UPDATE host_mdm_windows_profiles SET status = 'verified' WHERE profile_uuid = ? `
_ , err := q . ExecContext ( context . Background ( ) , stmt , teamProfiles [ 0 ] )
return err
} )
s . DoRaw ( "POST" , fmt . Sprintf ( "/api/latest/fleet/hosts/%d/configuration_profiles/%s/resend" , host . ID , teamProfiles [ 0 ] ) , nil , http . StatusAccepted )
// Trigger a profile sync, device gets the resent profile
verifyProfiles ( mdmDevice , 1 , false )
2024-04-18 21:01:37 +00:00
// add a macOS profile to the team
mcUUID := "a" + uuid . NewString ( )
2025-08-03 06:18:13 +00:00
prof := mcBytesForTest ( "name-" + mcUUID , "identifier-" + mcUUID , mcUUID )
2024-04-18 21:01:37 +00:00
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
stmt := ` INSERT INTO mdm_apple_configuration_profiles (profile_uuid, team_id, name, identifier, mobileconfig, checksum, uploaded_at) VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP); `
2025-08-03 06:18:13 +00:00
_ , err := q . ExecContext ( context . Background ( ) , stmt , mcUUID , tm . ID , "name-" + mcUUID , "identifier-" + mcUUID , prof , test . MakeTestBytes ( ) )
2024-04-18 21:01:37 +00:00
return err
} )
// trigger a profile sync, device doesn't get the macOS profile
verifyProfiles ( mdmDevice , 0 , false )
// can't resend a macOS profile to a Windows host
2024-11-27 20:39:55 +00:00
res = s . DoRaw ( "POST" , fmt . Sprintf ( "/api/latest/fleet/hosts/%d/configuration_profiles/%s/resend" , host . ID , mcUUID ) , nil , http . StatusUnprocessableEntity )
2024-04-18 21:01:37 +00:00
errMsg = extractServerErrorText ( res . Body )
require . Contains ( t , errMsg , "Profile is not compatible with host platform" )
}
func ( s * integrationMDMTestSuite ) TestApplyTeamsMDMWindowsProfiles ( ) {
t := s . T ( )
// create a team through the service so it initializes the agent ops
teamName := t . Name ( ) + "team1"
team := & fleet . Team {
Name : teamName ,
Description : "desc team1" ,
}
var createTeamResp teamResponse
s . DoJSON ( "POST" , "/api/latest/fleet/teams" , team , http . StatusOK , & createTeamResp )
require . NotZero ( t , createTeamResp . Team . ID )
team = createTeamResp . Team
rawTeamSpec := func ( mdmValue string ) json . RawMessage {
return json . RawMessage ( fmt . Sprintf ( ` { "specs": [ { "name": %q, "mdm": %s }] } ` , team . Name , mdmValue ) )
}
// set the windows custom settings fields
var applyResp applyTeamSpecsResponse
s . DoJSON ( "POST" , "/api/latest/fleet/spec/teams" , rawTeamSpec ( `
{
2024-06-25 19:26:28 +00:00
"windows_settings" : {
"custom_settings" : [
{ "path" : "foo" , "labels" : [ "baz" ] } ,
{ "path" : "bar" , "labels_exclude_any" : [ "x" , "y" ] }
]
}
2024-04-18 21:01:37 +00:00
}
` ) , http . StatusOK , & applyResp )
require . Len ( t , applyResp . TeamIDsByName , 1 )
// check that they are returned by a GET /config
var teamResp getTeamResponse
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/latest/fleet/teams/%d" , team . ID ) , nil , http . StatusOK , & teamResp )
2024-06-25 19:26:28 +00:00
require . ElementsMatch ( t , [ ] fleet . MDMProfileSpec {
{ Path : "foo" , LabelsIncludeAll : [ ] string { "baz" } } ,
{ Path : "bar" , LabelsExcludeAny : [ ] string { "x" , "y" } } ,
} , teamResp . Team . Config . MDM . WindowsSettings . CustomSettings . Value )
2024-04-18 21:01:37 +00:00
// patch without specifying the windows custom settings fields and an unrelated
// field, should not remove them
applyResp = applyTeamSpecsResponse { }
s . DoJSON ( "POST" , "/api/latest/fleet/spec/teams" , rawTeamSpec ( ` { "enable_disk_encryption": true } ` ) , http . StatusOK , & applyResp )
require . Len ( t , applyResp . TeamIDsByName , 1 )
// check that they are returned by a GET /config
teamResp = getTeamResponse { }
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/latest/fleet/teams/%d" , team . ID ) , nil , http . StatusOK , & teamResp )
2024-06-25 19:26:28 +00:00
require . ElementsMatch ( t , [ ] fleet . MDMProfileSpec {
{ Path : "foo" , LabelsIncludeAll : [ ] string { "baz" } } ,
{ Path : "bar" , LabelsExcludeAny : [ ] string { "x" , "y" } } ,
} , teamResp . Team . Config . MDM . WindowsSettings . CustomSettings . Value )
2024-04-18 21:01:37 +00:00
// patch with explicitly empty windows custom settings fields, would remove
// them but this is a dry-run
applyResp = applyTeamSpecsResponse { }
s . DoJSON ( "POST" , "/api/latest/fleet/spec/teams" , rawTeamSpec ( `
{ "windows_settings" : { "custom_settings" : null } }
` ) , http . StatusOK , & applyResp , "dry_run" , "true" )
assert . Equal ( t , map [ string ] uint { team . Name : team . ID } , applyResp . TeamIDsByName )
teamResp = getTeamResponse { }
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/latest/fleet/teams/%d" , team . ID ) , nil , http . StatusOK , & teamResp )
2024-06-25 19:26:28 +00:00
require . ElementsMatch ( t , [ ] fleet . MDMProfileSpec {
{ Path : "foo" , LabelsIncludeAll : [ ] string { "baz" } } ,
{ Path : "bar" , LabelsExcludeAny : [ ] string { "x" , "y" } } ,
} , teamResp . Team . Config . MDM . WindowsSettings . CustomSettings . Value )
2024-04-18 21:01:37 +00:00
// patch with explicitly empty windows custom settings fields, removes them
applyResp = applyTeamSpecsResponse { }
s . DoJSON ( "POST" , "/api/latest/fleet/spec/teams" , rawTeamSpec ( `
{ "windows_settings" : { "custom_settings" : null } }
` ) , http . StatusOK , & applyResp )
require . Len ( t , applyResp . TeamIDsByName , 1 )
teamResp = getTeamResponse { }
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/latest/fleet/teams/%d" , team . ID ) , nil , http . StatusOK , & teamResp )
require . Empty ( t , teamResp . Team . Config . MDM . WindowsSettings . CustomSettings . Value )
2024-06-25 19:26:28 +00:00
// apply with invalid mix of labels fails
res := s . Do ( "POST" , "/api/latest/fleet/spec/teams" , rawTeamSpec ( `
{
"windows_settings" : {
"custom_settings" : [
{ "path" : "foo" , "labels" : [ "a" ] , "labels_include_all" : [ "b" ] }
]
}
}
` ) , http . StatusUnprocessableEntity )
errMsg := extractServerErrorText ( res . Body )
2024-11-05 20:13:44 +00:00
assert . Contains ( t , errMsg , ` For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included. ` )
2024-04-18 21:01:37 +00:00
}
func ( s * integrationMDMTestSuite ) TestBatchSetMDMProfiles ( ) {
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 )
2024-06-18 17:02:00 +00:00
bigString := strings . Repeat ( "a" , 1024 * 1024 + 1 )
// Profile is too big
resp := s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload { { Contents : [ ] byte ( bigString ) } } } ,
http . StatusUnprocessableEntity )
2025-03-07 14:33:40 +00:00
require . Contains ( t , extractServerErrorText ( resp . Body ) , "Validation Failed: maximum configuration profile file size is 1 MB" )
2024-06-18 17:02:00 +00:00
2025-10-10 18:44:58 +00:00
// invalid profile (bad mobileconfig)
resp = s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{
Name : "Bad mobileconfig" , Contents : [ ] byte ( ` < ? xml version = "1.0" encoding = "UTF-8" ? >
< ! DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd" >
< plist version = "1.0" >
< dict >
< key > PayloadContent < / key >
< array / > ` ) ,
} ,
} } , http . StatusUnprocessableEntity )
require . Contains ( t , extractServerErrorText ( resp . Body ) , "Validation Failed: new MDMAppleConfigProfile: plist: error parsing XML property list: XML syntax error" )
2024-04-18 21:01:37 +00:00
// apply an empty set to no-team
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : nil } , http . StatusNoContent )
2024-08-30 21:00:35 +00:00
// Nothing changed, so no activity items
s . lastActivityOfTypeDoesNotMatch (
2024-04-18 21:01:37 +00:00
fleet . ActivityTypeEditedMacosProfile { } . ActivityName ( ) ,
` { "team_id": null, "team_name": null} ` ,
0 ,
)
2024-08-30 21:00:35 +00:00
s . lastActivityOfTypeDoesNotMatch (
2024-04-18 21:01:37 +00:00
fleet . ActivityTypeEditedWindowsProfile { } . ActivityName ( ) ,
` { "team_id": null, "team_name": null} ` ,
0 ,
)
2024-08-30 21:00:35 +00:00
s . lastActivityOfTypeDoesNotMatch (
2024-04-18 21:01:37 +00:00
fleet . ActivityTypeEditedDeclarationProfile { } . ActivityName ( ) ,
` { "team_id": null, "team_name": null} ` ,
0 ,
)
// apply to both team id and name
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : nil } ,
2024-10-18 17:38:26 +00:00
http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) , "team_name" , tm . Name )
2024-04-18 21:01:37 +00:00
// invalid team name
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : nil } ,
http . StatusNotFound , "team_name" , uuid . New ( ) . String ( ) )
// duplicate PayloadDisplayName
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : "N1" , Contents : mobileconfigForTest ( "N1" , "I1" ) } ,
{ Name : "N2" , Contents : mobileconfigForTest ( "N1" , "I2" ) } ,
{ Name : "N3" , Contents : syncMLForTest ( "./Foo/Bar" ) } ,
{ Name : "N4" , Contents : declarationForTest ( "D1" ) } ,
2024-10-18 17:38:26 +00:00
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
2024-04-18 21:01:37 +00:00
// profiles with reserved macOS identifiers
for p := range mobileconfig . FleetPayloadIdentifiers ( ) {
res := s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : "N1" , Contents : mobileconfigForTest ( "N1" , "I1" ) } ,
{ Name : p , Contents : mobileconfigForTest ( p , p ) } ,
{ Name : "N3" , Contents : syncMLForTest ( "./Foo/Bar" ) } ,
{ Name : "N4" , Contents : declarationForTest ( "D1" ) } ,
2024-10-18 17:38:26 +00:00
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
2024-04-18 21:01:37 +00:00
errMsg := extractServerErrorText ( res . Body )
2025-03-07 14:33:40 +00:00
require . Contains ( t , errMsg , fmt . Sprintf ( "Validation Failed: payload identifier %s is not allowed" , p ) )
2024-04-18 21:01:37 +00:00
}
// payloads with reserved types
for p := range mobileconfig . FleetPayloadTypes ( ) {
2025-03-25 00:36:36 +00:00
if p == mobileconfig . FleetCustomSettingsPayloadType {
2025-03-25 14:08:47 +00:00
// FileVault options in the custom settings payload are checked in file_vault_options_test.go
2025-03-25 00:36:36 +00:00
continue
}
2024-04-18 21:01:37 +00:00
res := s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : "N1" , Contents : mobileconfigForTestWithContent ( "N1" , "I1" , "II1" , p , "" ) } ,
{ Name : "N3" , Contents : syncMLForTest ( "./Foo/Bar" ) } ,
{ Name : "N4" , Contents : declarationForTest ( "D1" ) } ,
2024-10-18 17:38:26 +00:00
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
2024-04-18 21:01:37 +00:00
errMsg := extractServerErrorText ( res . Body )
2025-03-21 14:56:50 +00:00
switch p {
2025-03-25 00:36:36 +00:00
case mobileconfig . FleetFileVaultPayloadType , mobileconfig . FleetRecoveryKeyEscrowPayloadType :
2025-03-21 19:24:52 +00:00
assert . Contains ( t , errMsg , mobileconfig . DiskEncryptionProfileRestrictionErrMsg )
2025-03-21 14:56:50 +00:00
default :
assert . Contains ( t , errMsg , fmt . Sprintf ( "Validation Failed: unsupported PayloadType(s): %s" , p ) )
}
2024-04-18 21:01:37 +00:00
}
// payloads with reserved identifiers
for p := range mobileconfig . FleetPayloadIdentifiers ( ) {
res := s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : "N1" , Contents : mobileconfigForTestWithContent ( "N1" , "I1" , p , "random" , "" ) } ,
{ Name : "N3" , Contents : syncMLForTest ( "./Foo/Bar" ) } ,
{ Name : "N4" , Contents : declarationForTest ( "D1" ) } ,
2024-10-18 17:38:26 +00:00
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
2024-04-18 21:01:37 +00:00
errMsg := extractServerErrorText ( res . Body )
2025-03-07 14:33:40 +00:00
require . Contains ( t , errMsg , fmt . Sprintf ( "Validation Failed: unsupported PayloadIdentifier(s): %s" , p ) )
2024-04-18 21:01:37 +00:00
}
// profiles with forbidden declaration types
for dt := range fleet . ForbiddenDeclTypes {
res := s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : "N1" , Contents : mobileconfigForTest ( "N1" , "I1" ) } ,
{ Name : "N3" , Contents : syncMLForTest ( "./Foo/Bar" ) } ,
{ Name : "N4" , Contents : declarationForTestWithType ( "D1" , dt ) } ,
2024-10-18 17:38:26 +00:00
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
2024-04-18 21:01:37 +00:00
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/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : "N1" , Contents : mobileconfigForTest ( "N1" , "I1" ) } ,
{ Name : "N3" , Contents : syncMLForTest ( "./Foo/Bar" ) } ,
{ Name : "N4" , Contents : declarationForTestWithType ( "D1" , "com.apple.configuration.softwareupdate.enforcement.specific" ) } ,
2024-10-18 17:38:26 +00:00
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
2024-04-18 21:01:37 +00:00
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/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : "N1" , Contents : mobileconfigForTest ( "N1" , "I1" ) } ,
{ Name : "N3" , Contents : syncMLForTest ( "./Foo/Bar" ) } ,
{ Name : "N4" , Contents : [ ] byte ( ` { "foo":} ` ) } ,
2025-09-22 15:29:57 +00:00
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
2024-04-18 21:01:37 +00:00
errMsg = extractServerErrorText ( res . Body )
2025-09-22 15:29:57 +00:00
require . Contains ( t , errMsg , "N4 is not a valid macOS, Windows, or Android configuration profile" )
2024-04-18 21:01:37 +00:00
// profiles with reserved Windows location URIs
// bitlocker
res = s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : "N1" , Contents : mobileconfigForTest ( "N1" , "I1" ) } ,
{ Name : syncml . FleetBitLockerTargetLocURI , Contents : syncMLForTest ( fmt . Sprintf ( "%s/Foo" , syncml . FleetBitLockerTargetLocURI ) ) } ,
{ Name : "N3" , Contents : syncMLForTest ( "./Foo/Bar" ) } ,
2024-10-18 17:38:26 +00:00
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
2024-04-18 21:01:37 +00:00
errMsg = extractServerErrorText ( res . Body )
2025-03-21 19:24:52 +00:00
assert . Contains ( t , errMsg , syncml . DiskEncryptionProfileRestrictionErrMsg )
2024-04-18 21:01:37 +00:00
// os updates
res = s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : "N1" , Contents : mobileconfigForTest ( "N1" , "I1" ) } ,
{ Name : syncml . FleetOSUpdateTargetLocURI , Contents : syncMLForTest ( fmt . Sprintf ( "%s/Foo" , syncml . FleetOSUpdateTargetLocURI ) ) } ,
{ Name : "N3" , Contents : syncMLForTest ( "./Foo/Bar" ) } ,
2024-10-18 17:38:26 +00:00
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
2024-04-18 21:01:37 +00:00
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/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : "N3" , Contents : [ ] byte ( ` <Exec></Exec> ` ) } ,
2024-10-18 17:38:26 +00:00
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
2024-04-18 21:01:37 +00:00
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/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : "N3" , Contents : [ ] byte ( ` foo ` ) } ,
2024-10-18 17:38:26 +00:00
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
2024-04-18 21:01:37 +00:00
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/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : "N1" , Contents : mobileconfigForTest ( "N1" , "I1" ) } ,
{ Name : "N2" , Contents : syncMLForTest ( "./Foo/Bar" ) } ,
{ Name : "N4" , Contents : declarationForTest ( "D1" ) } ,
2024-10-18 17:38:26 +00:00
} } , http . StatusNoContent , "team_id" , fmt . Sprint ( tm . ID ) , "dry_run" , "true" )
2024-04-18 21:01:37 +00:00
s . assertConfigProfilesByIdentifier ( & tm . ID , "I1" , false )
s . assertWindowsConfigProfilesByName ( & tm . ID , "N1" , false )
// successfully apply for a team and verify activities
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : "N1" , Contents : mobileconfigForTest ( "N1" , "I1" ) } ,
{ Name : "N2" , Contents : syncMLForTest ( "./Foo/Bar" ) } ,
{ Name : "N4" , Contents : declarationForTest ( "D1" ) } ,
2024-10-18 17:38:26 +00:00
} } , http . StatusNoContent , "team_id" , fmt . Sprint ( tm . ID ) )
2024-04-18 21:01:37 +00:00
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 ,
)
2024-06-25 19:26:28 +00:00
// 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/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : "N1" , Contents : mobileconfigForTest ( "N1" , "I1" ) , Labels : [ ] 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/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : "N1" , Contents : mobileconfigForTest ( "N1" , "I1" ) , Labels : [ ] string { lbl1 . Name } , LabelsExcludeAny : [ ] string { lbl2 . Name } } ,
} } , http . StatusUnprocessableEntity )
msg = extractServerErrorText ( res . Body )
2024-11-05 20:13:44 +00:00
require . Contains ( t , msg , ` For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included. ` )
2024-06-25 19:26:28 +00:00
// successful batch-set
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : "N1" , Contents : mobileconfigForTest ( "N1" , "I1" ) , Labels : [ ] string { lbl1 . Name , lbl2 . Name } } ,
{ Name : "N2" , Contents : syncMLForTest ( "./Foo/Bar" ) , LabelsIncludeAll : [ ] string { lbl1 . Name } } ,
{ Name : "N4" , Contents : 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 ] . LabelsIncludeAll , [ ] 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/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : "N1" , Contents : mobileconfigForTest ( "N1" , "I1" ) , LabelsExcludeAny : [ ] string { lbl1 . Name , lbl3 . Name } } ,
{ Name : "N2" , Contents : 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 )
2024-04-18 21:01:37 +00:00
// 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 } } ,
2025-01-30 11:17:36 +00:00
expectErr : "More than one configuration profile have the same name 'N1'" ,
2024-04-18 21:01:37 +00:00
} ,
{
payload : [ ] fleet . MDMProfileBatchPayload { { Name : "N1" , Contents : declBytes } , { Name : "N1" , Contents : winBytes } } ,
2025-01-30 11:17:36 +00:00
expectErr : "More than one configuration profile have the same name 'N1'" ,
2024-04-18 21:01:37 +00:00
} ,
{
payload : [ ] fleet . MDMProfileBatchPayload { { Name : "N1" , Contents : mcBytes } , { Name : "N1" , Contents : declBytes } } ,
2025-01-30 11:17:36 +00:00
expectErr : "More than one configuration profile have the same name 'N1'" ,
2024-04-18 21:01:37 +00:00
} ,
} {
// team profiles
2024-10-18 17:38:26 +00:00
res = s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : p . payload } ,
http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
2024-04-18 21:01:37 +00:00
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 )
}
}
2025-09-08 13:52:30 +00:00
// 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":} ` ) } ,
2025-09-22 15:29:57 +00:00
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
2025-09-08 13:52:30 +00:00
errMsg = extractServerErrorText ( res . Body )
2025-09-22 15:29:57 +00:00
require . Contains ( t , errMsg , "N4 is not a valid macOS, Windows, or Android configuration profile." )
2025-09-08 13:52:30 +00:00
// 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 {
2025-12-18 14:49:02 +00:00
{ DisplayName : "NotRelevant" , Profile : mobileconfigForTest ( "N1" , "I1" ) } , // Check that we don't care about displayname for mobileconfig profiles
2025-09-08 13:52:30 +00:00
{ 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 )
}
2025-12-18 21:14:32 +00:00
// Get the current list of configuration profiles
var currentProfiles listMDMConfigProfilesResponse
s . DoJSON ( "GET" , "/api/latest/fleet/configuration_profiles" , nil , http . StatusOK , & currentProfiles )
require . Greater ( t , len ( currentProfiles . Profiles ) , 0 )
// Now we disable all three MDM's
appCfg , err := s . ds . AppConfig ( ctx )
require . NoError ( t , err )
appCfg . MDM . EnabledAndConfigured = false
appCfg . MDM . WindowsEnabledAndConfigured = false
appCfg . MDM . AndroidEnabledAndConfigured = false
err = s . ds . SaveAppConfig ( ctx , appCfg )
require . NoError ( t , err )
// Now do a batch with profiles in it, to see it fails trying to add.
s . Do ( "POST" , "/api/latest/fleet/configuration_profiles/batch" , batchModifyMDMConfigProfilesRequest { ConfigurationProfiles : [ ] fleet . BatchModifyMDMConfigProfilePayload {
{ DisplayName : "NEW" , Profile : mobileconfigForTest ( "NEW" , "INEW" ) } ,
} } , http . StatusUnprocessableEntity )
// Now do a batch without any profiles to ensure we can delete with all MDM's disabled.
s . Do ( "POST" , "/api/latest/fleet/configuration_profiles/batch" , batchModifyMDMConfigProfilesRequest { ConfigurationProfiles : [ ] fleet . BatchModifyMDMConfigProfilePayload { } } , http . StatusNoContent )
2025-09-08 13:52:30 +00:00
}
2024-04-18 21:01:37 +00:00
func ( s * integrationMDMTestSuite ) TestBatchSetMDMProfilesBackwardsCompat ( ) {
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 )
// apply an empty set to no-team
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , map [ string ] any { "profiles" : nil } , http . StatusNoContent )
2024-08-30 21:00:35 +00:00
// Nothing changed, so no activity
s . lastActivityOfTypeDoesNotMatch (
2024-04-18 21:01:37 +00:00
fleet . ActivityTypeEditedMacosProfile { } . ActivityName ( ) ,
` { "team_id": null, "team_name": null} ` ,
0 ,
)
2024-08-30 21:00:35 +00:00
s . lastActivityOfTypeDoesNotMatch (
2024-04-18 21:01:37 +00:00
fleet . ActivityTypeEditedWindowsProfile { } . ActivityName ( ) ,
` { "team_id": null, "team_name": null} ` ,
0 ,
)
// apply to both team id and name
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , map [ string ] any { "profiles" : nil } ,
2024-10-18 17:38:26 +00:00
http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) , "team_name" , tm . Name )
2024-04-18 21:01:37 +00:00
// invalid team name
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , map [ string ] any { "profiles" : nil } ,
http . StatusNotFound , "team_name" , uuid . New ( ) . String ( ) )
// duplicate PayloadDisplayName
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , map [ string ] any { "profiles" : map [ string ] [ ] byte {
"N1" : mobileconfigForTest ( "N1" , "I1" ) ,
"N2" : mobileconfigForTest ( "N1" , "I2" ) ,
"N3" : syncMLForTest ( "./Foo/Bar" ) ,
2024-10-18 17:38:26 +00:00
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
2024-04-18 21:01:37 +00:00
// profiles with reserved macOS identifiers
for p := range mobileconfig . FleetPayloadIdentifiers ( ) {
res := s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , map [ string ] any { "profiles" : map [ string ] [ ] byte {
"N1" : mobileconfigForTest ( "N1" , "I1" ) ,
p : mobileconfigForTest ( p , p ) ,
"N3" : syncMLForTest ( "./Foo/Bar" ) ,
2024-10-18 17:38:26 +00:00
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
2024-04-18 21:01:37 +00:00
errMsg := extractServerErrorText ( res . Body )
2025-03-07 14:33:40 +00:00
require . Contains ( t , errMsg , fmt . Sprintf ( "Validation Failed: payload identifier %s is not allowed" , p ) )
2024-04-18 21:01:37 +00:00
}
// payloads with reserved types
for p := range mobileconfig . FleetPayloadTypes ( ) {
2025-03-25 00:36:36 +00:00
if p == mobileconfig . FleetCustomSettingsPayloadType {
2025-03-25 14:08:47 +00:00
// FileVault options in the custom settings payload are checked in file_vault_options_test.go
2025-03-25 00:36:36 +00:00
continue
}
2024-04-18 21:01:37 +00:00
res := s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , map [ string ] any { "profiles" : map [ string ] [ ] byte {
"N1" : mobileconfigForTestWithContent ( "N1" , "I1" , "II1" , p , "" ) ,
"N3" : syncMLForTest ( "./Foo/Bar" ) ,
2024-10-18 17:38:26 +00:00
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
2024-04-18 21:01:37 +00:00
errMsg := extractServerErrorText ( res . Body )
2025-03-21 14:56:50 +00:00
switch p {
2025-03-25 00:36:36 +00:00
case mobileconfig . FleetFileVaultPayloadType , mobileconfig . FleetRecoveryKeyEscrowPayloadType :
2025-03-21 19:24:52 +00:00
assert . Contains ( t , errMsg , mobileconfig . DiskEncryptionProfileRestrictionErrMsg )
2025-03-21 14:56:50 +00:00
default :
assert . Contains ( t , errMsg , fmt . Sprintf ( "Validation Failed: unsupported PayloadType(s): %s" , p ) )
}
2024-04-18 21:01:37 +00:00
}
// payloads with reserved identifiers
for p := range mobileconfig . FleetPayloadIdentifiers ( ) {
res := s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , map [ string ] any { "profiles" : map [ string ] [ ] byte {
"N1" : mobileconfigForTestWithContent ( "N1" , "I1" , p , "random" , "" ) ,
"N3" : syncMLForTest ( "./Foo/Bar" ) ,
2024-10-18 17:38:26 +00:00
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
2024-04-18 21:01:37 +00:00
errMsg := extractServerErrorText ( res . Body )
2025-03-07 14:33:40 +00:00
require . Contains ( t , errMsg , fmt . Sprintf ( "Validation Failed: unsupported PayloadIdentifier(s): %s" , p ) )
2024-04-18 21:01:37 +00:00
}
// profiles with reserved Windows location URIs
// bitlocker
res := s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , map [ string ] any { "profiles" : map [ string ] [ ] byte {
"N1" : mobileconfigForTest ( "N1" , "I1" ) ,
syncml . FleetBitLockerTargetLocURI : syncMLForTest ( fmt . Sprintf ( "%s/Foo" , syncml . FleetBitLockerTargetLocURI ) ) ,
"N3" : syncMLForTest ( "./Foo/Bar" ) ,
2024-10-18 17:38:26 +00:00
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
2024-04-18 21:01:37 +00:00
errMsg := extractServerErrorText ( res . Body )
2025-03-21 19:24:52 +00:00
assert . Contains ( t , errMsg , syncml . DiskEncryptionProfileRestrictionErrMsg )
2024-04-18 21:01:37 +00:00
// os updates
res = s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , map [ string ] any { "profiles" : map [ string ] [ ] byte {
"N1" : mobileconfigForTest ( "N1" , "I1" ) ,
syncml . FleetOSUpdateTargetLocURI : syncMLForTest ( fmt . Sprintf ( "%s/Foo" , syncml . FleetOSUpdateTargetLocURI ) ) ,
"N3" : syncMLForTest ( "./Foo/Bar" ) ,
2024-10-18 17:38:26 +00:00
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
2024-04-18 21:01:37 +00:00
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/v1/fleet/mdm/profiles/batch" , map [ string ] any { "profiles" : map [ string ] [ ] byte {
"N3" : [ ] byte ( ` <Exec></Exec> ` ) ,
2024-10-18 17:38:26 +00:00
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
2024-04-18 21:01:37 +00:00
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/v1/fleet/mdm/profiles/batch" , map [ string ] any { "profiles" : map [ string ] [ ] byte {
"N3" : [ ] byte ( ` foo ` ) ,
2024-10-18 17:38:26 +00:00
} } , http . StatusUnprocessableEntity , "team_id" , fmt . Sprint ( tm . ID ) )
2024-04-18 21:01:37 +00:00
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/v1/fleet/mdm/profiles/batch" , map [ string ] any { "profiles" : map [ string ] [ ] byte {
"N1" : mobileconfigForTest ( "N1" , "I1" ) ,
"N2" : syncMLForTest ( "./Foo/Bar" ) ,
2024-10-18 17:38:26 +00:00
} } , http . StatusNoContent , "team_id" , fmt . Sprint ( tm . ID ) , "dry_run" , "true" )
2024-04-18 21:01:37 +00:00
s . assertConfigProfilesByIdentifier ( & tm . ID , "I1" , false )
s . assertWindowsConfigProfilesByName ( & tm . ID , "N1" , false )
// successfully apply for a team and verify activities
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , map [ string ] any { "profiles" : map [ string ] [ ] byte {
"N1" : mobileconfigForTest ( "N1" , "I1" ) ,
"N2" : syncMLForTest ( "./Foo/Bar" ) ,
2024-10-18 17:38:26 +00:00
} } , http . StatusNoContent , "team_id" , fmt . Sprint ( tm . ID ) )
2024-04-18 21:01:37 +00:00
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 ,
)
}
func ( s * integrationMDMTestSuite ) TestMDMBatchSetProfilesKeepsReservedNames ( ) {
t := s . T ( )
ctx := context . Background ( )
checkMacProfs := func ( teamID * uint , names ... string ) {
var count int
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
var tid uint
if teamID != nil {
tid = * teamID
}
return sqlx . GetContext ( ctx , q , & count , ` SELECT COUNT(*) FROM mdm_apple_configuration_profiles WHERE team_id = ? ` , tid )
} )
require . Equal ( t , len ( names ) , count )
for _ , n := range names {
s . assertMacOSConfigProfilesByName ( teamID , n , true )
}
}
checkWinProfs := func ( teamID * uint , names ... string ) {
var count int
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
var tid uint
if teamID != nil {
tid = * teamID
}
return sqlx . GetContext ( ctx , q , & count , ` SELECT COUNT(*) FROM mdm_windows_configuration_profiles WHERE team_id = ? ` , tid )
} )
for _ , n := range names {
s . assertWindowsConfigProfilesByName ( teamID , n , true )
}
}
acResp := appConfigResponse { }
s . DoJSON ( "GET" , "/api/latest/fleet/config" , nil , http . StatusOK , & acResp )
require . True ( t , acResp . MDM . EnabledAndConfigured )
require . True ( t , acResp . MDM . WindowsEnabledAndConfigured )
// ensures that the fleetd profile is created
secrets , err := s . ds . GetEnrollSecrets ( ctx , nil )
require . NoError ( t , err )
if len ( secrets ) == 0 {
require . NoError ( t , s . ds . ApplyEnrollSecrets ( ctx , nil , [ ] * fleet . EnrollSecret { { Secret : t . Name ( ) } } ) )
}
2024-05-30 21:18:42 +00:00
require . NoError ( t , ReconcileAppleProfiles ( ctx , s . ds , s . mdmCommander , s . logger ) )
2024-04-18 21:01:37 +00:00
// turn on disk encryption and os updates
s . DoJSON ( "PATCH" , "/api/latest/fleet/config" , json . RawMessage ( ` {
"mdm" : {
"enable_disk_encryption" : true ,
"windows_updates" : {
"deadline_days" : 3 ,
"grace_period_days" : 1
} ,
"macos_updates" : {
"deadline" : "2023-12-31" ,
"minimum_version" : "13.3.7"
}
}
} ` ) , http . StatusOK , & acResp )
checkMacProfs ( nil , servermdm . ListFleetReservedMacOSProfileNames ( ) ... )
checkWinProfs ( nil , servermdm . ListFleetReservedWindowsProfileNames ( ) ... )
// batch set only windows profiles doesn't remove the reserved names
2025-04-18 12:45:18 +00:00
newWinProfile := syncml . ForTestWithData ( [ ] syncml . TestCommand { { Verb : "Replace" , LocURI : "l1" , Data : "d1" } } )
2024-04-18 21:01:37 +00:00
var testProfiles [ ] fleet . MDMProfileBatchPayload
testProfiles = append ( testProfiles , fleet . MDMProfileBatchPayload {
Name : "n1" ,
Contents : newWinProfile ,
} )
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : testProfiles } , http . StatusNoContent )
checkMacProfs ( nil , servermdm . ListFleetReservedMacOSProfileNames ( ) ... )
checkWinProfs ( nil , append ( servermdm . ListFleetReservedWindowsProfileNames ( ) , "n1" ) ... )
// batch set windows and mac profiles doesn't remove the reserved names
newMacProfile := mcBytesForTest ( "n2" , "i2" , uuid . NewString ( ) )
testProfiles = append ( testProfiles , fleet . MDMProfileBatchPayload {
Name : "n2" ,
Contents : newMacProfile ,
} )
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : testProfiles } , http . StatusNoContent )
checkMacProfs ( nil , append ( servermdm . ListFleetReservedMacOSProfileNames ( ) , "n2" ) ... )
checkWinProfs ( nil , append ( servermdm . ListFleetReservedWindowsProfileNames ( ) , "n1" ) ... )
// batch set only mac profiles doesn't remove the reserved names
testProfiles = [ ] fleet . MDMProfileBatchPayload { {
Name : "n2" ,
Contents : newMacProfile ,
} }
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : testProfiles } , http . StatusNoContent )
checkMacProfs ( nil , append ( servermdm . ListFleetReservedMacOSProfileNames ( ) , "n2" ) ... )
checkWinProfs ( nil , servermdm . ListFleetReservedWindowsProfileNames ( ) ... )
// create a team
var tmResp teamResponse
s . DoJSON ( "POST" , "/api/v1/fleet/teams" , map [ string ] string { "Name" : t . Name ( ) } , http . StatusOK , & tmResp )
// edit team mdm config to turn on disk encryption and os updates
s . DoJSON ( "PATCH" , fmt . Sprintf ( "/api/latest/fleet/teams/%d" , tmResp . Team . ID ) , modifyTeamRequest {
TeamPayload : fleet . TeamPayload {
Name : ptr . String ( t . Name ( ) ) ,
MDM : & fleet . TeamPayloadMDM {
EnableDiskEncryption : optjson . SetBool ( true ) ,
WindowsUpdates : & fleet . WindowsUpdates {
DeadlineDays : optjson . SetInt ( 4 ) ,
GracePeriodDays : optjson . SetInt ( 1 ) ,
} ,
2024-07-24 17:34:23 +00:00
MacOSUpdates : & fleet . AppleOSUpdateSettings {
2024-04-18 21:01:37 +00:00
Deadline : optjson . SetString ( "2023-12-31" ) ,
MinimumVersion : optjson . SetString ( "13.3.8" ) ,
2025-12-16 18:38:28 +00:00
UpdateNewHosts : optjson . SetBool ( true ) ,
2024-04-18 21:01:37 +00:00
} ,
} ,
} ,
} , http . StatusOK , & teamResponse { } )
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/v1/fleet/teams/%d" , tmResp . Team . ID ) , nil , http . StatusOK , & tmResp )
require . True ( t , tmResp . Team . Config . MDM . EnableDiskEncryption )
require . Equal ( t , 4 , tmResp . Team . Config . MDM . WindowsUpdates . DeadlineDays . Value )
require . Equal ( t , 1 , tmResp . Team . Config . MDM . WindowsUpdates . GracePeriodDays . Value )
require . Equal ( t , "2023-12-31" , tmResp . Team . Config . MDM . MacOSUpdates . Deadline . Value )
require . Equal ( t , "13.3.8" , tmResp . Team . Config . MDM . MacOSUpdates . MinimumVersion . Value )
2025-12-16 18:38:28 +00:00
require . Equal ( t , true , tmResp . Team . Config . MDM . MacOSUpdates . UpdateNewHosts . Value )
2024-04-18 21:01:37 +00:00
2024-05-30 21:18:42 +00:00
require . NoError ( t , ReconcileAppleProfiles ( ctx , s . ds , s . mdmCommander , s . logger ) )
2024-04-18 21:01:37 +00:00
checkMacProfs ( & tmResp . Team . ID , servermdm . ListFleetReservedMacOSProfileNames ( ) ... )
checkWinProfs ( & tmResp . Team . ID , servermdm . ListFleetReservedWindowsProfileNames ( ) ... )
// batch set only windows profiles doesn't remove the reserved names
var testTeamProfiles [ ] fleet . MDMProfileBatchPayload
testTeamProfiles = append ( testTeamProfiles , fleet . MDMProfileBatchPayload {
Name : "n1" ,
Contents : newWinProfile ,
} )
2024-10-18 17:38:26 +00:00
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : testTeamProfiles } , http . StatusNoContent ,
"team_id" , fmt . Sprint ( tmResp . Team . ID ) )
2024-04-18 21:01:37 +00:00
checkMacProfs ( & tmResp . Team . ID , servermdm . ListFleetReservedMacOSProfileNames ( ) ... )
checkWinProfs ( & tmResp . Team . ID , append ( servermdm . ListFleetReservedWindowsProfileNames ( ) , "n1" ) ... )
// batch set windows and mac profiles doesn't remove the reserved names
testTeamProfiles = append ( testTeamProfiles , fleet . MDMProfileBatchPayload {
Name : "n2" ,
Contents : newMacProfile ,
} )
2024-10-18 17:38:26 +00:00
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : testTeamProfiles } , http . StatusNoContent ,
"team_id" , fmt . Sprint ( tmResp . Team . ID ) )
2024-04-18 21:01:37 +00:00
checkMacProfs ( & tmResp . Team . ID , append ( servermdm . ListFleetReservedMacOSProfileNames ( ) , "n2" ) ... )
checkWinProfs ( & tmResp . Team . ID , append ( servermdm . ListFleetReservedWindowsProfileNames ( ) , "n1" ) ... )
// batch set only mac profiles doesn't remove the reserved names
testTeamProfiles = [ ] fleet . MDMProfileBatchPayload { {
Name : "n2" ,
Contents : newMacProfile ,
} }
2024-10-18 17:38:26 +00:00
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : testTeamProfiles } , http . StatusNoContent ,
"team_id" , fmt . Sprint ( tmResp . Team . ID ) )
2024-04-18 21:01:37 +00:00
checkMacProfs ( & tmResp . Team . ID , append ( servermdm . ListFleetReservedMacOSProfileNames ( ) , "n2" ) ... )
checkWinProfs ( & tmResp . Team . ID , servermdm . ListFleetReservedWindowsProfileNames ( ) ... )
}
func ( s * integrationMDMTestSuite ) TestMDMAppleConfigProfileCRUD ( ) {
t := s . T ( )
ctx := context . Background ( )
testTeam , err := s . ds . NewTeam ( ctx , & fleet . Team { Name : "TestTeam" } )
require . NoError ( t , err )
2024-10-01 16:32:41 +00:00
teamDelete , err := s . ds . NewTeam ( ctx , & fleet . Team { Name : "TeamDelete" } )
require . NoError ( t , err )
2024-04-18 21:01:37 +00:00
testProfiles := make ( map [ string ] fleet . MDMAppleConfigProfile )
generateTestProfile := func ( name string , identifier string ) {
i := identifier
if i == "" {
i = fmt . Sprintf ( "%s.SomeIdentifier" , name )
}
cp := fleet . MDMAppleConfigProfile {
Name : name ,
Identifier : i ,
}
cp . Mobileconfig = mcBytesForTest ( cp . Name , cp . Identifier , fmt . Sprintf ( "%s.UUID" , name ) )
testProfiles [ name ] = cp
}
setTestProfileID := func ( name string , id uint ) {
tp := testProfiles [ name ]
tp . ProfileID = id
testProfiles [ name ] = tp
}
generateNewReq := func ( name string , teamID * uint ) ( * bytes . Buffer , map [ string ] string ) {
args := map [ string ] [ ] string { }
if teamID != nil {
args [ "team_id" ] = [ ] string { fmt . Sprintf ( "%d" , * teamID ) }
}
return generateNewProfileMultipartRequest ( t , "some_filename" , testProfiles [ name ] . Mobileconfig , s . token , args )
}
checkGetResponse := func ( resp * http . Response , expected fleet . MDMAppleConfigProfile ) {
// check expected headers
require . Contains ( t , resp . Header [ "Content-Type" ] , "application/x-apple-aspen-config" )
require . Contains ( t , resp . Header [ "Content-Disposition" ] , fmt . Sprintf ( ` attachment;filename="%s_%s.%s" ` , time . Now ( ) . Format ( "2006-01-02" ) , strings . ReplaceAll ( expected . Name , " " , "_" ) , "mobileconfig" ) )
// check expected body
var bb bytes . Buffer
_ , err = io . Copy ( & bb , resp . Body )
require . NoError ( t , err )
require . Equal ( t , [ ] byte ( expected . Mobileconfig ) , bb . Bytes ( ) )
}
checkConfigProfile := func ( expected fleet . MDMAppleConfigProfile , actual fleet . MDMAppleConfigProfile ) {
require . Equal ( t , expected . Name , actual . Name )
require . Equal ( t , expected . Identifier , actual . Identifier )
}
2024-10-01 16:32:41 +00:00
host , _ := createHostThenEnrollMDM ( s . ds , s . server . URL , t )
s . Do ( "POST" , "/api/latest/fleet/hosts/transfer" , addHostsToTeamRequest {
TeamID : & teamDelete . ID ,
HostIDs : [ ] uint { host . ID } ,
} , http . StatusOK )
2024-04-18 21:01:37 +00:00
// create new profile (no team)
generateTestProfile ( "TestNoTeam" , "" )
body , headers := generateNewReq ( "TestNoTeam" , nil )
newResp := s . DoRawWithHeaders ( "POST" , "/api/latest/fleet/mdm/apple/profiles" , body . Bytes ( ) , http . StatusOK , headers )
var newCP fleet . MDMAppleConfigProfile
err = json . NewDecoder ( newResp . Body ) . Decode ( & newCP )
require . NoError ( t , err )
require . NotEmpty ( t , newCP . ProfileID )
setTestProfileID ( "TestNoTeam" , newCP . ProfileID )
// create new profile (with team id)
generateTestProfile ( "TestWithTeamID" , "" )
body , headers = generateNewReq ( "TestWithTeamID" , & testTeam . ID )
newResp = s . DoRawWithHeaders ( "POST" , "/api/latest/fleet/mdm/apple/profiles" , body . Bytes ( ) , http . StatusOK , headers )
err = json . NewDecoder ( newResp . Body ) . Decode ( & newCP )
require . NoError ( t , err )
require . NotEmpty ( t , newCP . ProfileID )
setTestProfileID ( "TestWithTeamID" , newCP . ProfileID )
2024-10-01 16:32:41 +00:00
// Create a profile that we're going to remove immediately
generateTestProfile ( "TestImmediateDelete" , "" )
body , headers = generateNewReq ( "TestImmediateDelete" , & teamDelete . ID )
newResp = s . DoRawWithHeaders ( "POST" , "/api/latest/fleet/mdm/apple/profiles" , body . Bytes ( ) , http . StatusOK , headers )
newCP = fleet . MDMAppleConfigProfile { }
err = json . NewDecoder ( newResp . Body ) . Decode ( & newCP )
require . NoError ( t , err )
require . NotEmpty ( t , newCP . ProfileID )
setTestProfileID ( "TestImmediateDelete" , newCP . ProfileID )
// check that host_mdm_apple_profiles entry was created
var hostResp getHostResponse
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/latest/fleet/hosts/%d" , host . ID ) , nil , http . StatusOK , & hostResp )
require . NotNil ( t , hostResp . Host . MDM . Profiles )
require . Len ( t , * hostResp . Host . MDM . Profiles , 1 )
require . Equal ( t , ( * hostResp . Host . MDM . Profiles ) [ 0 ] . Name , "TestImmediateDelete" )
// now delete the profile before it's sent, we should see the host_mdm_apple_profiles entry go
// away
deletedCP := testProfiles [ "TestImmediateDelete" ]
deletePath := fmt . Sprintf ( "/api/latest/fleet/mdm/apple/profiles/%d" , deletedCP . ProfileID )
var deleteResp deleteMDMAppleConfigProfileResponse
s . DoJSON ( "DELETE" , deletePath , nil , http . StatusOK , & deleteResp )
// confirm deleted
var listResp listMDMAppleConfigProfilesResponse
s . DoJSON ( "GET" , "/api/latest/fleet/mdm/apple/profiles" , listMDMAppleConfigProfilesRequest { TeamID : teamDelete . ID } , http . StatusOK , & listResp )
require . Len ( t , listResp . ConfigProfiles , 0 )
getPath := fmt . Sprintf ( "/api/latest/fleet/mdm/apple/profiles/%d" , deletedCP . ProfileID )
_ = s . DoRawWithHeaders ( "GET" , getPath , nil , http . StatusNotFound , map [ string ] string { "Authorization" : fmt . Sprintf ( "Bearer %s" , s . token ) } )
// confirm no host profiles
hostResp = getHostResponse { }
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/latest/fleet/hosts/%d" , host . ID ) , nil , http . StatusOK , & hostResp )
require . Nil ( t , hostResp . Host . MDM . Profiles )
2024-04-18 21:01:37 +00:00
// list profiles (no team)
expectedCP := testProfiles [ "TestNoTeam" ]
s . DoJSON ( "GET" , "/api/latest/fleet/mdm/apple/profiles" , nil , http . StatusOK , & listResp )
require . Len ( t , listResp . ConfigProfiles , 1 )
respCP := listResp . ConfigProfiles [ 0 ]
require . Equal ( t , expectedCP . Name , respCP . Name )
checkConfigProfile ( expectedCP , * respCP )
require . Empty ( t , respCP . Mobileconfig ) // list profiles endpoint shouldn't include mobileconfig bytes
require . Empty ( t , respCP . TeamID ) // zero means no team
// list profiles (team 1)
expectedCP = testProfiles [ "TestWithTeamID" ]
listResp = listMDMAppleConfigProfilesResponse { }
s . DoJSON ( "GET" , "/api/latest/fleet/mdm/apple/profiles" , listMDMAppleConfigProfilesRequest { TeamID : testTeam . ID } , http . StatusOK , & listResp )
require . Len ( t , listResp . ConfigProfiles , 1 )
respCP = listResp . ConfigProfiles [ 0 ]
require . Equal ( t , expectedCP . Name , respCP . Name )
checkConfigProfile ( expectedCP , * respCP )
require . Empty ( t , respCP . Mobileconfig ) // list profiles endpoint shouldn't include mobileconfig bytes
require . Equal ( t , testTeam . ID , * respCP . TeamID ) // team 1
// get profile (no team)
expectedCP = testProfiles [ "TestNoTeam" ]
2024-10-01 16:32:41 +00:00
getPath = fmt . Sprintf ( "/api/latest/fleet/mdm/apple/profiles/%d" , expectedCP . ProfileID )
2024-04-18 21:01:37 +00:00
getResp := s . DoRawWithHeaders ( "GET" , getPath , nil , http . StatusOK , map [ string ] string { "Authorization" : fmt . Sprintf ( "Bearer %s" , s . token ) } )
checkGetResponse ( getResp , expectedCP )
// get profile (team 1)
expectedCP = testProfiles [ "TestWithTeamID" ]
getPath = fmt . Sprintf ( "/api/latest/fleet/mdm/apple/profiles/%d" , expectedCP . ProfileID )
getResp = s . DoRawWithHeaders ( "GET" , getPath , nil , http . StatusOK , map [ string ] string { "Authorization" : fmt . Sprintf ( "Bearer %s" , s . token ) } )
checkGetResponse ( getResp , expectedCP )
// delete profile (no team)
2024-10-01 16:32:41 +00:00
deletedCP = testProfiles [ "TestNoTeam" ]
deletePath = fmt . Sprintf ( "/api/latest/fleet/mdm/apple/profiles/%d" , deletedCP . ProfileID )
2024-04-18 21:01:37 +00:00
s . DoJSON ( "DELETE" , deletePath , nil , http . StatusOK , & deleteResp )
// confirm deleted
listResp = listMDMAppleConfigProfilesResponse { }
s . DoJSON ( "GET" , "/api/latest/fleet/mdm/apple/profiles" , listMDMAppleConfigProfilesRequest { } , http . StatusOK , & listResp )
require . Len ( t , listResp . ConfigProfiles , 0 )
getPath = fmt . Sprintf ( "/api/latest/fleet/mdm/apple/profiles/%d" , deletedCP . ProfileID )
_ = s . DoRawWithHeaders ( "GET" , getPath , nil , http . StatusNotFound , map [ string ] string { "Authorization" : fmt . Sprintf ( "Bearer %s" , s . token ) } )
// delete profile (team 1)
deletedCP = testProfiles [ "TestWithTeamID" ]
deletePath = fmt . Sprintf ( "/api/latest/fleet/mdm/apple/profiles/%d" , deletedCP . ProfileID )
deleteResp = deleteMDMAppleConfigProfileResponse { }
s . DoJSON ( "DELETE" , deletePath , nil , http . StatusOK , & deleteResp )
// confirm deleted
listResp = listMDMAppleConfigProfilesResponse { }
s . DoJSON ( "GET" , "/api/latest/fleet/mdm/apple/profiles" , listMDMAppleConfigProfilesRequest { TeamID : testTeam . ID } , http . StatusOK , & listResp )
require . Len ( t , listResp . ConfigProfiles , 0 )
getPath = fmt . Sprintf ( "/api/latest/fleet/mdm/apple/profiles/%d" , deletedCP . ProfileID )
_ = s . DoRawWithHeaders ( "GET" , getPath , nil , http . StatusNotFound , map [ string ] string { "Authorization" : fmt . Sprintf ( "Bearer %s" , s . token ) } )
2024-12-17 22:14:12 +00:00
// fail to create new profile (no team), invalid fleet secret
testProfiles [ "badSecrets" ] = fleet . MDMAppleConfigProfile {
Name : "badSecrets" ,
Identifier : "badSecrets.One" ,
Mobileconfig : mobileconfig . Mobileconfig ( ` < ? xml version = "1.0" encoding = "UTF-8" ? >
< ! DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd" >
< plist version = "1.0" >
< dict >
< key > PayloadContent < / key >
< array / >
< key > PayloadDisplayName < / key >
< string > badSecrets < / string >
< key > PayloadIdentifier < / key >
< string > badSecrets . One < / string >
< key > PayloadType < / key >
< string > Configuration < / string >
< key > PayloadUUID < / key >
< string > $ FLEET_SECRET_INVALID .35E2029 E - A0C2 - 4754 - B709 - 4 CAAB1B8D3CB < / string >
< key > PayloadVersion < / key >
< integer > 1 < / integer >
< / dict >
< / plist >
` ) ,
}
body , headers = generateNewReq ( "badSecrets" , nil )
newResp = s . DoRawWithHeaders ( "POST" , "/api/latest/fleet/mdm/apple/profiles" , body . Bytes ( ) , http . StatusUnprocessableEntity , headers )
errMsg := extractServerErrorText ( newResp . Body )
require . Contains ( t , errMsg , "$FLEET_SECRET_INVALID" )
2024-04-18 21:01:37 +00:00
// trying to add/delete profiles with identifiers managed by Fleet fails
for p := range mobileconfig . FleetPayloadIdentifiers ( ) {
generateTestProfile ( "TestNoTeam" , p )
body , headers := generateNewReq ( "TestNoTeam" , nil )
s . DoRawWithHeaders ( "POST" , "/api/latest/fleet/mdm/apple/profiles" , body . Bytes ( ) , http . StatusBadRequest , headers )
generateTestProfile ( "TestWithTeamID" , p )
body , headers = generateNewReq ( "TestWithTeamID" , nil )
s . DoRawWithHeaders ( "POST" , "/api/latest/fleet/mdm/apple/profiles" , body . Bytes ( ) , http . StatusBadRequest , headers )
cp , err := fleet . NewMDMAppleConfigProfile ( mobileconfigForTestWithContent ( "N1" , "I1" , p , "random" , "" ) , nil )
require . NoError ( t , err )
testProfiles [ "WithContent" ] = * cp
body , headers = generateNewReq ( "WithContent" , nil )
s . DoRawWithHeaders ( "POST" , "/api/latest/fleet/mdm/apple/profiles" , body . Bytes ( ) , http . StatusBadRequest , headers )
}
// trying to add profiles with identifiers managed by Fleet fails
for p := range mobileconfig . FleetPayloadIdentifiers ( ) {
generateTestProfile ( "TestNoTeam" , p )
body , headers := generateNewReq ( "TestNoTeam" , nil )
s . DoRawWithHeaders ( "POST" , "/api/latest/fleet/mdm/apple/profiles" , body . Bytes ( ) , http . StatusBadRequest , headers )
generateTestProfile ( "TestWithTeamID" , p )
body , headers = generateNewReq ( "TestWithTeamID" , nil )
s . DoRawWithHeaders ( "POST" , "/api/latest/fleet/mdm/apple/profiles" , body . Bytes ( ) , http . StatusBadRequest , headers )
cp , err := fleet . NewMDMAppleConfigProfile ( mobileconfigForTestWithContent ( "N1" , "I1" , p , "random" , "" ) , nil )
require . NoError ( t , err )
testProfiles [ "WithContent" ] = * cp
body , headers = generateNewReq ( "WithContent" , nil )
s . DoRawWithHeaders ( "POST" , "/api/latest/fleet/mdm/apple/profiles" , body . Bytes ( ) , http . StatusBadRequest , headers )
}
// trying to add profiles with names reserved by Fleet fails
for name := range servermdm . FleetReservedProfileNames ( ) {
cp := & fleet . MDMAppleConfigProfile {
Name : name ,
Identifier : "valid.identifier" ,
Mobileconfig : mcBytesForTest ( name , "valid.identifier" , "some-uuid" ) ,
}
body , headers := generateNewProfileMultipartRequest ( t , "some_filename" , cp . Mobileconfig , s . token , nil )
s . DoRawWithHeaders ( "POST" , "/api/latest/fleet/mdm/apple/profiles" , body . Bytes ( ) , http . StatusBadRequest , headers )
body , headers = generateNewProfileMultipartRequest ( t , "some_filename" , cp . Mobileconfig , s . token , map [ string ] [ ] string {
"team_id" : { fmt . Sprintf ( "%d" , testTeam . ID ) } ,
} )
s . DoRawWithHeaders ( "POST" , "/api/latest/fleet/mdm/apple/profiles" , body . Bytes ( ) , http . StatusBadRequest , headers )
cp , err := fleet . NewMDMAppleConfigProfile ( mobileconfigForTestWithContent (
"valid outer name" ,
"valid.outer.identifier" ,
"valid.inner.identifer" ,
"some-uuid" ,
name ,
) , nil )
require . NoError ( t , err )
body , headers = generateNewProfileMultipartRequest ( t , "some_filename" , cp . Mobileconfig , s . token , nil )
s . DoRawWithHeaders ( "POST" , "/api/latest/fleet/mdm/apple/profiles" , body . Bytes ( ) , http . StatusBadRequest , headers )
cp . TeamID = & testTeam . ID
body , headers = generateNewProfileMultipartRequest ( t , "some_filename" , cp . Mobileconfig , s . token , map [ string ] [ ] string {
"team_id" : { fmt . Sprintf ( "%d" , testTeam . ID ) } ,
} )
s . DoRawWithHeaders ( "POST" , "/api/latest/fleet/mdm/apple/profiles" , body . Bytes ( ) , http . StatusBadRequest , headers )
}
// make fleet add a FileVault profile
acResp := appConfigResponse { }
s . DoJSON ( "PATCH" , "/api/latest/fleet/config" , json . RawMessage ( ` {
"mdm" : { "enable_disk_encryption" : true }
} ` ) , http . StatusOK , & acResp )
assert . True ( t , acResp . MDM . EnableDiskEncryption . Value )
profile := s . assertConfigProfilesByIdentifier ( nil , mobileconfig . FleetFileVaultPayloadIdentifier , true )
// try to delete the profile
deletePath = fmt . Sprintf ( "/api/latest/fleet/mdm/apple/profiles/%d" , profile . ProfileID )
deleteResp = deleteMDMAppleConfigProfileResponse { }
s . DoJSON ( "DELETE" , deletePath , nil , http . StatusBadRequest , & deleteResp )
}
2024-07-03 14:20:33 +00:00
func ( s * integrationMDMTestSuite ) TestHostMDMProfilesExcludeLabels ( ) {
t := s . T ( )
2025-01-14 22:45:10 +00:00
s . setSkipWorkerJobs ( t )
2024-07-03 14:20:33 +00:00
ctx := context . Background ( )
triggerReconcileProfiles := func ( ) {
s . awaitTriggerProfileSchedule ( t )
// this will only mark them as "pending", as the response to confirm
// profile deployment is asynchronous, so we simulate it here by
// updating any "pending" (not NULL) profiles to "verifying"
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
if _ , err := q . ExecContext ( ctx , ` UPDATE host_mdm_apple_profiles SET status = ? WHERE status = ? ` , fleet . OSSettingsVerifying , fleet . OSSettingsPending ) ; err != nil {
return err
}
if _ , err := q . ExecContext ( ctx , ` UPDATE host_mdm_apple_declarations SET status = ? WHERE status = ? ` , fleet . OSSettingsVerifying , fleet . OSSettingsPending ) ; err != nil {
return err
}
if _ , err := q . ExecContext ( ctx , ` UPDATE host_mdm_windows_profiles SET status = ? WHERE status = ? ` , fleet . OSSettingsVerifying , fleet . OSSettingsPending ) ; err != nil {
return err
}
return nil
} )
}
// run the crons immediately, will create the Fleet-controlled profiles that
// will then be expected to be applied (e.g. com.fleetdm.fleetd.config and
// com.fleetdm.caroot)
// first create the no-team enroll secret (required to create the fleet profiles)
var applyResp applyEnrollSecretSpecResponse
s . DoJSON ( "POST" , "/api/latest/fleet/spec/enroll_secret" ,
applyEnrollSecretSpecRequest {
Spec : & fleet . EnrollSecretSpec { Secrets : [ ] * fleet . EnrollSecret { { Secret : "super-global-secret" } } } ,
} , http . StatusOK , & applyResp )
s . awaitTriggerProfileSchedule ( t )
// create an Apple and a Windows host
appleHost , _ := createHostThenEnrollMDM ( s . ds , s . server . URL , t )
windowsHost , _ := createWindowsHostThenEnrollMDM ( s . ds , s . server . URL , t )
// create a few labels
labels := make ( [ ] * fleet . Label , 5 )
for i := 0 ; i < len ( labels ) ; i ++ {
label , err := s . ds . NewLabel ( ctx , & fleet . Label { Name : fmt . Sprintf ( "label-%d" , i ) , Query : "select 1;" } )
require . NoError ( t , err )
labels [ i ] = label
}
2024-11-05 16:58:31 +00:00
// simulate reporting label results for those hosts
appleHost . LabelUpdatedAt = time . Now ( )
windowsHost . LabelUpdatedAt = time . Now ( )
err := s . ds . UpdateHost ( ctx , appleHost )
require . NoError ( t , err )
err = s . ds . UpdateHost ( ctx , windowsHost )
require . NoError ( t , err )
2024-07-03 14:20:33 +00:00
// set an Apple profile and declaration and a Windows profile
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : "A1" , Contents : mobileconfigForTest ( "A1" , "A1" ) , LabelsExcludeAny : [ ] string { labels [ 0 ] . Name , labels [ 1 ] . Name } } ,
{ Name : "W2" , Contents : syncMLForTest ( "./Foo/W2" ) , LabelsExcludeAny : [ ] string { labels [ 2 ] . Name , labels [ 3 ] . Name } } ,
{ Name : "D3" , Contents : declarationForTest ( "D3" ) , LabelsExcludeAny : [ ] string { labels [ 4 ] . Name } } ,
} } , http . StatusNoContent )
// hosts are not members of any label yet, so running the cron applies the labels
s . awaitTriggerProfileSchedule ( t )
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
appleHost : {
{ Identifier : "A1" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "D3" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
} ,
} )
s . assertHostWindowsConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMWindowsProfile {
windowsHost : {
{ Name : "W2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
} ,
} )
// simulate the reconcile profiles deployment
triggerReconcileProfiles ( )
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
appleHost : {
{ Identifier : "A1" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : "D3" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
} )
s . assertHostWindowsConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMWindowsProfile {
windowsHost : {
{ Name : "W2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
} )
// mark some profiles as verified (despite accepting a HostMacOSProfile struct, it supports Windows too)
2024-11-05 16:58:31 +00:00
err = apple_mdm . VerifyHostMDMProfiles ( ctx , s . ds , appleHost , map [ string ] * fleet . HostMacOSProfile {
2024-07-03 14:20:33 +00:00
"A1" : { Identifier : "A1" , DisplayName : "A1" , InstallDate : time . Now ( ) } ,
} )
require . NoError ( t , err )
err = apple_mdm . VerifyHostMDMProfiles ( ctx , s . ds , windowsHost , map [ string ] * fleet . HostMacOSProfile {
"W2" : { Identifier : "W2" , DisplayName : "W2" , InstallDate : time . Now ( ) } ,
} )
require . NoError ( t , err )
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
appleHost : {
{ Identifier : "A1" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerified } ,
{ Identifier : "D3" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
} )
s . assertHostWindowsConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMWindowsProfile {
windowsHost : {
{ Name : "W2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerified } ,
} ,
} )
// make hosts members of labels [1], [2], [3] and [4], meaning that none of the profiles apply anymore
err = s . ds . AsyncBatchInsertLabelMembership ( ctx , [ ] [ 2 ] uint {
{ labels [ 1 ] . ID , appleHost . ID } ,
{ labels [ 2 ] . ID , appleHost . ID } ,
{ labels [ 3 ] . ID , appleHost . ID } ,
{ labels [ 4 ] . ID , appleHost . ID } ,
{ labels [ 1 ] . ID , windowsHost . ID } ,
{ labels [ 2 ] . ID , windowsHost . ID } ,
{ labels [ 3 ] . ID , windowsHost . ID } ,
{ labels [ 4 ] . ID , windowsHost . ID } ,
} )
require . NoError ( t , err )
s . awaitTriggerProfileSchedule ( t )
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
appleHost : {
{ Identifier : "A1" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "D3" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
} )
// windows profiles go straight to removed without getting deleted on the host
s . assertHostWindowsConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMWindowsProfile {
windowsHost : { } ,
} )
// remove membership of labels [2] for Windows, and [4] for Apple, meaning
// that only D3 will be installed on Apple (as the Windows host is still
// member of an excluded label)
err = s . ds . AsyncBatchDeleteLabelMembership ( ctx , [ ] [ 2 ] uint {
{ labels [ 4 ] . ID , appleHost . ID } ,
{ labels [ 2 ] . ID , windowsHost . ID } ,
} )
require . NoError ( t , err )
s . awaitTriggerProfileSchedule ( t )
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
appleHost : {
{ Identifier : "A1" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "D3" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
} )
s . assertHostWindowsConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMWindowsProfile {
windowsHost : { } ,
} )
// remove label [3] as an excluded label for the Windows profile, meaning
// that the host now meets the requirement to install.
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : "A1" , Contents : mobileconfigForTest ( "A1" , "A1" ) , LabelsExcludeAny : [ ] string { labels [ 0 ] . Name , labels [ 1 ] . Name } } ,
{ Name : "W2" , Contents : syncMLForTest ( "./Foo/W2" ) , LabelsExcludeAny : [ ] string { labels [ 2 ] . Name } } ,
{ Name : "D3" , Contents : declarationForTest ( "D3" ) , LabelsExcludeAny : [ ] string { labels [ 4 ] . Name } } ,
} } , http . StatusNoContent )
s . awaitTriggerProfileSchedule ( t )
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
appleHost : {
{ Identifier : "A1" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "D3" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
} )
s . assertHostWindowsConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMWindowsProfile {
windowsHost : {
{ Name : "W2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
} ,
} )
// simulate the reconcile profiles deployment and mark as verified
triggerReconcileProfiles ( )
err = apple_mdm . VerifyHostMDMProfiles ( ctx , s . ds , windowsHost , map [ string ] * fleet . HostMacOSProfile {
"W2" : { Identifier : "W2" , DisplayName : "W2" , InstallDate : time . Now ( ) } ,
} )
require . NoError ( t , err )
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
appleHost : {
{ Identifier : "D3" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
} )
s . assertHostWindowsConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMWindowsProfile {
windowsHost : {
{ Name : "W2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerified } ,
} ,
} )
// break the A1 profile by deleting labels [1]
2025-12-30 03:28:45 +00:00
err = s . ds . DeleteLabel ( ctx , labels [ 1 ] . Name , fleet . TeamFilter { User : & fleet . User { GlobalRole : ptr . String ( fleet . RoleAdmin ) } } )
2024-07-03 14:20:33 +00:00
require . NoError ( t , err )
// it doesn't get installed to the Apple host, as it is broken
triggerReconcileProfiles ( )
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
appleHost : {
{ Identifier : "D3" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
} )
s . assertHostWindowsConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMWindowsProfile {
windowsHost : {
{ Name : "W2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerified } ,
} ,
} )
// it also doesn't get installed to a new host not a member of any labels
appleHost2 , _ := createHostThenEnrollMDM ( s . ds , s . server . URL , t )
triggerReconcileProfiles ( )
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
appleHost : {
{ Identifier : "D3" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
appleHost2 : {
{ Identifier : "D3" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
} )
s . assertHostWindowsConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMWindowsProfile {
windowsHost : {
{ Name : "W2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerified } ,
} ,
} )
// delete labels [2] and [4], breaking D3 and W2, they don't get removed
// since they are broken
2025-12-30 03:28:45 +00:00
err = s . ds . DeleteLabel ( ctx , labels [ 2 ] . Name , fleet . TeamFilter { User : & fleet . User { GlobalRole : ptr . String ( fleet . RoleAdmin ) } } )
2024-07-03 14:20:33 +00:00
require . NoError ( t , err )
2025-12-30 03:28:45 +00:00
err = s . ds . DeleteLabel ( ctx , labels [ 4 ] . Name , fleet . TeamFilter { User : & fleet . User { GlobalRole : ptr . String ( fleet . RoleAdmin ) } } )
2024-07-03 14:20:33 +00:00
require . NoError ( t , err )
triggerReconcileProfiles ( )
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
appleHost : {
{ Identifier : "D3" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
appleHost2 : {
{ Identifier : "D3" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
} )
s . assertHostWindowsConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMWindowsProfile {
windowsHost : {
{ Name : "W2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerified } ,
} ,
} )
}
2024-08-30 13:04:10 +00:00
2024-11-14 14:30:32 +00:00
func ( s * integrationMDMTestSuite ) TestMDMProfilesIncludeAnyLabels ( ) {
t := s . T ( )
ctx := context . Background ( )
triggerReconcileProfiles := func ( ) {
s . awaitTriggerProfileSchedule ( t )
// this will only mark them as "pending", as the response to confirm
// profile deployment is asynchronous, so we simulate it here by
// updating any "pending" (not NULL) profiles to "verifying"
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
if _ , err := q . ExecContext ( ctx , ` UPDATE host_mdm_apple_profiles SET status = ? WHERE status = ? ` , fleet . OSSettingsVerifying , fleet . OSSettingsPending ) ; err != nil {
return err
}
if _ , err := q . ExecContext ( ctx , ` UPDATE host_mdm_apple_declarations SET status = ? WHERE status = ? ` , fleet . OSSettingsVerifying , fleet . OSSettingsPending ) ; err != nil {
return err
}
if _ , err := q . ExecContext ( ctx , ` UPDATE host_mdm_windows_profiles SET status = ? WHERE status = ? ` , fleet . OSSettingsVerifying , fleet . OSSettingsPending ) ; err != nil {
return err
}
return nil
} )
}
// run the crons immediately, will create the Fleet-controlled profiles that
// will then be expected to be applied (e.g. com.fleetdm.fleetd.config and
// com.fleetdm.caroot)
// first create the no-team enroll secret (required to create the fleet profiles)
var applyResp applyEnrollSecretSpecResponse
s . DoJSON ( "POST" , "/api/latest/fleet/spec/enroll_secret" ,
applyEnrollSecretSpecRequest {
Spec : & fleet . EnrollSecretSpec { Secrets : [ ] * fleet . EnrollSecret { { Secret : "super-global-secret" } } } ,
} , http . StatusOK , & applyResp )
s . awaitTriggerProfileSchedule ( t )
// create an Apple and a Windows host
appleHost , _ := createHostThenEnrollMDM ( s . ds , s . server . URL , t )
windowsHost , _ := createWindowsHostThenEnrollMDM ( s . ds , s . server . URL , t )
// create a few labels, we'll use the first five for "exclude any" profiles and the remaining for "include any"
labels := make ( [ ] * fleet . Label , 10 )
for i := 0 ; i < len ( labels ) ; i ++ {
label , err := s . ds . NewLabel ( ctx , & fleet . Label { Name : fmt . Sprintf ( "label-%d" , i ) , Query : "select 1;" } )
require . NoError ( t , err )
labels [ i ] = label
}
// simulate reporting label results for those hosts
appleHost . LabelUpdatedAt = time . Now ( )
windowsHost . LabelUpdatedAt = time . Now ( )
err := s . ds . UpdateHost ( ctx , appleHost )
require . NoError ( t , err )
err = s . ds . UpdateHost ( ctx , windowsHost )
require . NoError ( t , err )
// set up some Apple profiles and declarations and Windows profiles
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : "A1" , Contents : mobileconfigForTest ( "A1" , "A1" ) , LabelsIncludeAny : [ ] string { labels [ 0 ] . Name , labels [ 1 ] . Name } } ,
{ Name : "W2" , Contents : syncMLForTest ( "./Foo/W2" ) , LabelsIncludeAny : [ ] string { labels [ 2 ] . Name , labels [ 3 ] . Name } } ,
{ Name : "D3" , Contents : declarationForTest ( "D3" ) , LabelsIncludeAny : [ ] string { labels [ 4 ] . Name } } ,
} } , http . StatusNoContent )
// hosts are not members of any label yet, so running the cron applies no labels
s . awaitTriggerProfileSchedule ( t )
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
appleHost : {
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
} ,
} )
s . assertHostWindowsConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMWindowsProfile {
windowsHost : { } ,
} )
// make hosts members of labels [1], [2], [3] and [4], meaning that each of the "include any"
// labels will now match at least one host
err = s . ds . AsyncBatchInsertLabelMembership ( ctx , [ ] [ 2 ] uint {
{ labels [ 0 ] . ID , appleHost . ID } ,
{ labels [ 1 ] . ID , appleHost . ID } ,
{ labels [ 2 ] . ID , appleHost . ID } ,
{ labels [ 3 ] . ID , appleHost . ID } ,
{ labels [ 4 ] . ID , appleHost . ID } ,
{ labels [ 1 ] . ID , windowsHost . ID } ,
{ labels [ 2 ] . ID , windowsHost . ID } ,
{ labels [ 3 ] . ID , windowsHost . ID } ,
{ labels [ 4 ] . ID , windowsHost . ID } ,
} )
require . NoError ( t , err )
triggerReconcileProfiles ( )
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
appleHost : {
{ Identifier : "A1" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : "D3" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
} )
s . assertHostWindowsConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMWindowsProfile {
windowsHost : {
{ Name : "W2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
} )
// remove membership of labels [2] for Windows, and [1] and [4] for Apple, meaning
// that D3 will be removed on Apple, A1 will remain on Apple because the host is still a member
// of [0], and W2 will remain on Windows because the host is still a member of [3]
err = s . ds . AsyncBatchDeleteLabelMembership ( ctx , [ ] [ 2 ] uint {
{ labels [ 1 ] . ID , appleHost . ID } ,
{ labels [ 4 ] . ID , appleHost . ID } ,
{ labels [ 2 ] . ID , windowsHost . ID } ,
} )
require . NoError ( t , err )
s . awaitTriggerProfileSchedule ( t )
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
appleHost : {
{ Identifier : "A1" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : "D3" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
} )
s . assertHostWindowsConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMWindowsProfile {
windowsHost : {
{ Name : "W2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
} ,
} )
}
2024-08-30 13:04:10 +00:00
func ( s * integrationMDMTestSuite ) TestOTAProfile ( ) {
t := s . T ( )
ctx := context . Background ( )
2024-09-05 13:54:54 +00:00
// Getting profile for non-existent secret it's ok
s . Do ( "GET" , "/api/latest/fleet/enrollment_profiles/ota" , getOTAProfileRequest { } , http . StatusOK , "enroll_secret" , "not-real" )
2024-08-30 13:04:10 +00:00
// Create an enroll secret; has some special characters that should be escaped in the profile
globalEnrollSec := "global_enroll+_/sec"
escSec := url . QueryEscape ( globalEnrollSec )
s . Do ( "POST" , "/api/latest/fleet/spec/enroll_secret" , applyEnrollSecretSpecRequest {
Spec : & fleet . EnrollSecretSpec {
Secrets : [ ] * fleet . EnrollSecret { { Secret : globalEnrollSec } } ,
} ,
} , http . StatusOK )
cfg , err := s . ds . AppConfig ( ctx )
require . NoError ( t , err )
2025-08-18 16:31:53 +00:00
t . Run ( "gets profile with idp uuid included if boyd cookie is set" , func ( t * testing . T ) {
// Get profile with that enroll secret
j , err := json . Marshal ( getOTAProfileRequest { } )
require . NoError ( t , err )
idpUUID := uuid . New ( )
resp := s . DoRawWithHeaders ( "GET" , "/api/latest/fleet/enrollment_profiles/ota" , j , http . StatusOK , map [ string ] string {
"Cookie" : fmt . Sprintf ( "%s=%s" , shared_mdm . BYODIdpCookieName , idpUUID . String ( ) ) ,
} , "enroll_secret" , globalEnrollSec )
require . NotZero ( t , resp . ContentLength )
require . Contains ( t , resp . Header . Get ( "Content-Disposition" ) , ` attachment;filename="fleet-mdm-enrollment-profile.mobileconfig" ` )
require . Contains ( t , resp . Header . Get ( "Content-Type" ) , "application/x-apple-aspen-config" )
require . Contains ( t , resp . Header . Get ( "X-Content-Type-Options" ) , "nosniff" )
defer resp . Body . Close ( )
b , err := io . ReadAll ( resp . Body )
require . NoError ( t , err )
require . Equal ( t , resp . ContentLength , int64 ( len ( b ) ) )
require . Contains ( t , string ( b ) , "com.fleetdm.fleet.mdm.apple.ota" )
require . Contains ( t , string ( b ) , fmt . Sprintf ( "%s/api/v1/fleet/ota_enrollment?enroll_secret=%s&idp_uuid=%s" , cfg . ServerSettings . ServerURL , escSec , idpUUID . String ( ) ) )
require . Contains ( t , string ( b ) , cfg . OrgInfo . OrgName )
} )
t . Run ( "does not include idp_uuid in the url if cookie is not set" , func ( t * testing . T ) {
resp := s . Do ( "GET" , "/api/latest/fleet/enrollment_profiles/ota" , & getOTAProfileRequest { } , http . StatusOK , "enroll_secret" , globalEnrollSec )
require . NotZero ( t , resp . ContentLength )
require . Contains ( t , resp . Header . Get ( "Content-Disposition" ) , ` attachment;filename="fleet-mdm-enrollment-profile.mobileconfig" ` )
require . Contains ( t , resp . Header . Get ( "Content-Type" ) , "application/x-apple-aspen-config" )
require . Contains ( t , resp . Header . Get ( "X-Content-Type-Options" ) , "nosniff" )
b , err := io . ReadAll ( resp . Body )
require . NoError ( t , err )
require . Equal ( t , resp . ContentLength , int64 ( len ( b ) ) )
require . Contains ( t , string ( b ) , "com.fleetdm.fleet.mdm.apple.ota" )
require . Contains ( t , string ( b ) , fmt . Sprintf ( "%s/api/v1/fleet/ota_enrollment?enroll_secret=%s" , cfg . ServerSettings . ServerURL , escSec ) )
require . NotContains ( t , string ( b ) , "idp_uuid=" )
require . Contains ( t , string ( b ) , cfg . OrgInfo . OrgName )
} )
2024-08-30 13:04:10 +00:00
}
2024-12-30 19:07:32 +00:00
// TestAppleDDMSecretVariablesUpload tests uploading DDM profiles with secrets via the /configuration_profiles endpoint
func ( s * integrationMDMTestSuite ) TestAppleDDMSecretVariablesUpload ( ) {
tmpl := `
{
"Type" : "com.apple.configuration.decl%d" ,
"Identifier" : "com.fleet.config%d" ,
"Payload" : {
"ServiceType" : "com.apple.bash%d" ,
"DataAssetReference" : "com.fleet.asset.bash"
}
} `
newProfileBytes := func ( i int ) [ ] byte {
return [ ] byte ( fmt . Sprintf ( tmpl , i , i , i ) )
}
getProfileContents := func ( profileUUID string ) string {
profile , err := s . ds . GetMDMAppleDeclaration ( context . Background ( ) , profileUUID )
require . NoError ( s . T ( ) , err )
2024-12-30 23:58:39 +00:00
assert . NotNil ( s . T ( ) , profile . SecretsUpdatedAt )
2024-12-30 19:07:32 +00:00
return string ( profile . RawJSON )
}
s . testSecretVariablesUpload ( newProfileBytes , getProfileContents , "json" , "darwin" )
}
func ( s * integrationMDMTestSuite ) testSecretVariablesUpload ( newProfileBytes func ( i int ) [ ] byte ,
2025-01-30 11:17:36 +00:00
getProfileContents func ( profileUUID string ) string , fileExtension string , platform string ,
) {
2024-12-30 19:07:32 +00:00
t := s . T ( )
const numProfiles = 2
var profiles [ ] [ ] byte
for i := 0 ; i < numProfiles ; i ++ {
profiles = append ( profiles , newProfileBytes ( i ) )
}
// Use secrets
myBash := "com.apple.bash0"
profiles [ 0 ] = [ ] byte ( strings . ReplaceAll ( string ( profiles [ 0 ] ) , myBash , "$" + fleet . ServerSecretPrefix + "BASH" ) )
secretProfile := profiles [ 1 ]
profiles [ 1 ] = [ ] byte ( "${" + fleet . ServerSecretPrefix + "PROFILE}" )
body , headers := generateNewProfileMultipartRequest (
t , "secret-config0." + fileExtension , profiles [ 0 ] , s . token , nil ,
)
res := s . DoRawWithHeaders ( "POST" , "/api/latest/fleet/configuration_profiles" , body . Bytes ( ) , http . StatusUnprocessableEntity , headers )
assertBodyContains ( t , res , ` Secret variable \"$FLEET_SECRET_BASH\" missing ` )
// Add secret(s) to server
2025-08-14 22:33:47 +00:00
req := createSecretVariablesRequest {
2024-12-30 19:07:32 +00:00
SecretVariables : [ ] fleet . SecretVariable {
{
Name : "FLEET_SECRET_BASH" ,
Value : myBash ,
} ,
{
Name : "FLEET_SECRET_PROFILE" ,
Value : string ( secretProfile ) ,
} ,
} ,
}
2025-08-14 22:33:47 +00:00
secretResp := createSecretVariablesResponse { }
2024-12-30 19:07:32 +00:00
s . DoJSON ( "PUT" , "/api/latest/fleet/spec/secret_variables" , req , http . StatusOK , & secretResp )
res = s . DoRawWithHeaders ( "POST" , "/api/latest/fleet/configuration_profiles" , body . Bytes ( ) , http . StatusOK , headers )
var resp newMDMConfigProfileResponse
err := json . NewDecoder ( res . Body ) . Decode ( & resp )
require . NoError ( t , err )
assert . NotEmpty ( t , resp . ProfileUUID )
body , headers = generateNewProfileMultipartRequest (
t , "secret-config1." + fileExtension , profiles [ 1 ] , s . token , nil ,
)
s . DoJSON ( "PUT" , "/api/latest/fleet/spec/secret_variables" , req , http . StatusOK , & secretResp )
res = s . DoRawWithHeaders ( "POST" , "/api/latest/fleet/configuration_profiles" , body . Bytes ( ) , http . StatusOK , headers )
err = json . NewDecoder ( res . Body ) . Decode ( & resp )
require . NoError ( t , err )
assert . NotEmpty ( t , resp . ProfileUUID )
var listResp listMDMConfigProfilesResponse
s . DoJSON ( "GET" , "/api/latest/fleet/mdm/profiles" , & listMDMConfigProfilesRequest { } , http . StatusOK , & listResp )
require . Len ( t , listResp . Profiles , numProfiles )
profileUUIDs := make ( [ ] string , numProfiles )
for _ , p := range listResp . Profiles {
switch p . Name {
case "secret-config0" :
assert . Equal ( t , platform , p . Platform )
profileUUIDs [ 0 ] = p . ProfileUUID
case "secret-config1" :
assert . Equal ( t , platform , p . Platform )
profileUUIDs [ 1 ] = p . ProfileUUID
default :
t . Errorf ( "unexpected profile %s" , p . Name )
}
}
// Check that contents are masking secret values
for i := 0 ; i < numProfiles ; i ++ {
assert . Equal ( t , string ( profiles [ i ] ) , getProfileContents ( profileUUIDs [ i ] ) )
}
// Delete profiles -- make sure there is no issue deleting profiles with secrets
for i := 0 ; i < numProfiles ; i ++ {
s . Do ( "DELETE" , "/api/latest/fleet/configuration_profiles/" + profileUUIDs [ i ] , nil , http . StatusOK )
}
s . DoJSON ( "GET" , "/api/latest/fleet/mdm/profiles" , & listMDMConfigProfilesRequest { } , http . StatusOK , & listResp )
require . Empty ( t , listResp . Profiles )
}
// TestAppleConfigSecretVariablesUpload tests uploading Apple config profiles with secrets via the /configuration_profiles endpoint
func ( s * integrationMDMTestSuite ) TestAppleConfigSecretVariablesUpload ( ) {
tmpl := `
< ? xml version = "1.0" encoding = "UTF-8" ? >
< ! DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd" >
< plist version = "1.0" >
< dict >
< key > PayloadDescription < / key >
< string > For secret variables < / string >
< key > PayloadDisplayName < / key >
< string > secret - config % d < / string >
< key > PayloadIdentifier < / key >
< string > PI % d < / string >
< key > PayloadType < / key >
< string > Configuration < / string >
< key > PayloadUUID < / key >
< string > % d < / string >
< key > PayloadVersion < / key >
< integer > 1 < / integer >
< key > PayloadContent < / key >
< array >
< dict >
< key > Bash < / key >
< string > $ FLEET_SECRET_BASH < / string >
< key > PayloadDisplayName < / key >
< string > secret payload < / string >
< key > PayloadIdentifier < / key >
< string > com . test . secret < / string >
< key > PayloadType < / key >
< string > com . test . secretd < / string >
< key > PayloadUUID < / key >
< string > 476 F5334 - D501 - 4768 - 9 A31 - 1 A18A4E1E808 < / string >
< key > PayloadVersion < / key >
< integer > 1 < / integer >
< / dict >
< / array >
< / dict >
< / plist > `
newProfileBytes := func ( i int ) [ ] byte {
return [ ] byte ( fmt . Sprintf ( tmpl , i , i , i ) )
}
getProfileContents := func ( profileUUID string ) string {
profile , err := s . ds . GetMDMAppleConfigProfile ( context . Background ( ) , profileUUID )
require . NoError ( s . T ( ) , err )
2024-12-30 23:58:39 +00:00
assert . NotNil ( s . T ( ) , profile . SecretsUpdatedAt )
2024-12-30 19:07:32 +00:00
return string ( profile . Mobileconfig )
}
s . testSecretVariablesUpload ( newProfileBytes , getProfileContents , "mobileconfig" , "darwin" )
}
// TestWindowsConfigSecretVariablesUpload tests uploading Windows profiles with secrets via the /configuration_profiles endpoint
func ( s * integrationMDMTestSuite ) TestWindowsConfigSecretVariablesUpload ( ) {
tmpl := `
< Replace >
< Item >
< Meta >
< Format xmlns = "syncml:metinf" > int < / Format >
< / Meta >
< Target >
< LocURI > . / Device / Vendor / MSFT / Policy / Config / System / DisableOneDriveFileSync < / LocURI >
< / Target >
< Data > $ FLEET_SECRET_BASH < / Data >
< / Item >
< / Replace >
`
newProfileBytes := func ( i int ) [ ] byte {
return [ ] byte ( fmt . Sprintf ( tmpl , i , i , i ) )
}
getProfileContents := func ( profileUUID string ) string {
profile , err := s . ds . GetMDMWindowsConfigProfile ( context . Background ( ) , profileUUID )
require . NoError ( s . T ( ) , err )
return string ( profile . SyncML )
}
s . testSecretVariablesUpload ( newProfileBytes , getProfileContents , "xml" , "windows" )
}
2025-01-06 19:16:34 +00:00
func ( s * integrationMDMTestSuite ) TestAppleProfileDeletion ( ) {
t := s . T ( )
ctx := context . Background ( )
err := s . ds . ApplyEnrollSecrets ( ctx , nil , [ ] * fleet . EnrollSecret { { Secret : t . Name ( ) } } )
require . NoError ( t , err )
globalProfiles := [ ] [ ] byte {
mobileconfigForTest ( "N1" , "I1" ) ,
}
wantGlobalProfiles := globalProfiles
wantGlobalProfiles = append (
wantGlobalProfiles ,
setupExpectedFleetdProfile ( t , s . server . URL , t . Name ( ) , nil ) ,
)
// add global profiles
s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" , batchSetMDMAppleProfilesRequest { Profiles : globalProfiles } , http . StatusNoContent )
// Create a host and then enroll to MDM.
host , mdmDevice := createHostThenEnrollMDM ( s . ds , s . server . URL , t )
// Add IdP email to host
mysql . ExecAdhocSQL ( t , s . ds , func ( e sqlx . ExtContext ) error {
_ , err := e . ExecContext ( ctx , ` INSERT INTO host_emails (email, host_id, source) VALUES (?, ?, ?) ` , "idp@example.com" , host . ID ,
fleet . DeviceMappingMDMIdpAccounts )
return err
} )
// trigger a profile sync
s . awaitTriggerProfileSchedule ( t )
installs , removes := checkNextPayloads ( t , mdmDevice , false )
// verify that we received all profiles
s . signedProfilesMatch (
append ( wantGlobalProfiles , setupExpectedCAProfile ( t , s . ds ) ) ,
installs ,
)
require . Empty ( t , removes )
// Add a profile with a Fleet variable. We are also testing that removal of a profile with a Fleet variable works.
// A unique command is created for each host when this Fleet variable is used.
globalProfilesPlusOne := [ ] [ ] byte {
globalProfiles [ 0 ] ,
2025-08-10 10:24:38 +00:00
mobileconfigForTest ( "N2" , "$FLEET_VAR_" + string ( fleet . FleetVarHostEndUserEmailIDP ) ) ,
2025-01-06 19:16:34 +00:00
}
2025-04-30 20:03:23 +00:00
// via the deprecated endpoint, this fails because variables are not supported
res := s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" , batchSetMDMAppleProfilesRequest { Profiles : globalProfilesPlusOne } ,
http . StatusUnprocessableEntity )
errMsg := extractServerErrorText ( res . Body )
require . Contains ( t , errMsg , "profile variables are not supported by this deprecated endpoint" )
// via the new endpoint, this works
s . Do ( "POST" , "/api/latest/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : "N1" , Contents : globalProfilesPlusOne [ 0 ] } ,
{ Name : "N2" , Contents : globalProfilesPlusOne [ 1 ] } ,
} } , http . StatusNoContent )
2025-01-06 19:16:34 +00:00
// trigger a profile sync
s . awaitTriggerProfileSchedule ( t )
// Make sure profile was uploaded
profiles , err := s . ds . GetHostMDMAppleProfiles ( ctx , host . UUID )
require . NoError ( t , err )
assert . Len ( t , profiles , 4 )
// Delete a profile before it is sent to device
s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" , batchSetMDMAppleProfilesRequest { Profiles : globalProfiles } , http . StatusNoContent )
// trigger a profile sync
s . awaitTriggerProfileSchedule ( t )
2025-05-21 14:50:38 +00:00
2025-01-06 19:16:34 +00:00
sendErrorOnRemoveProfile := func ( device * mdmtest . TestAppleMDMClient ) {
// The host grabs the removal command from Fleet
cmd , err := device . Idle ( )
require . NoError ( t , err )
assert . Equal ( t , "RemoveProfile" , cmd . Command . RequestType )
// Since profile is not on the device, it returns an error.
errChain := [ ] mdm . ErrorChain {
{
ErrorCode : 89 ,
ErrorDomain : "FooErrorDomain" ,
LocalizedDescription : "The profile not found" ,
} ,
}
cmd , err = device . Err ( cmd . CommandUUID , errChain )
require . NoError ( t , err )
assert . Nil ( t , cmd )
}
sendErrorOnRemoveProfile ( mdmDevice )
// Make sure deleted profile no longer shows up
profiles , err = s . ds . GetHostMDMAppleProfiles ( ctx , host . UUID )
require . NoError ( t , err )
assert . Len ( t , profiles , 3 )
// Add a profile again
2025-04-30 20:03:23 +00:00
s . Do ( "POST" , "/api/latest/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : "N1" , Contents : globalProfilesPlusOne [ 0 ] } ,
{ Name : "N2" , Contents : globalProfilesPlusOne [ 1 ] } ,
} } , http . StatusNoContent )
2025-01-06 19:16:34 +00:00
// trigger a profile sync
s . awaitTriggerProfileSchedule ( t )
// The host grabs the profile from Fleet
cmd , err := mdmDevice . Idle ( )
require . NoError ( t , err )
assert . Equal ( t , "InstallProfile" , cmd . Command . RequestType )
// Verify that the Fleet variable was replaced with the IdP email
type Command struct {
Command struct {
Payload [ ] byte
}
}
var p Command
err = plist . Unmarshal ( cmd . Raw , & p )
require . NoError ( t , err )
2025-04-29 18:35:37 +00:00
assert . NotContains ( t , string ( p . Command . Payload ) , "$FLEET_VAR_" + fleet . FleetVarHostEndUserEmailIDP )
2025-01-06 19:16:34 +00:00
assert . Contains ( t , string ( p . Command . Payload ) , "idp@example.com" )
// While the host is installing the profile, we delete it.
s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" , batchSetMDMAppleProfilesRequest { Profiles : globalProfiles } , http . StatusNoContent )
// trigger a profile sync
s . awaitTriggerProfileSchedule ( t )
// Host acknowledges installing the profile and grabs the remove command
cmd , err = mdmDevice . Acknowledge ( cmd . CommandUUID )
require . NoError ( t , err )
assert . Equal ( t , "RemoveProfile" , cmd . Command . RequestType )
cmd , err = mdmDevice . Acknowledge ( cmd . CommandUUID )
require . NoError ( t , err )
assert . Nil ( t , cmd )
// Add another device
host2 , mdmDevice2 := createHostThenEnrollMDM ( s . ds , s . server . URL , t )
// Add IdP email to host
mysql . ExecAdhocSQL ( t , s . ds , func ( e sqlx . ExtContext ) error {
_ , err := e . ExecContext ( ctx , ` INSERT INTO host_emails (email, host_id, source) VALUES (?, ?, ?) ` , "idp2@example.com" , host2 . ID ,
fleet . DeviceMappingMDMIdpAccounts )
return err
} )
// trigger a profile sync
s . awaitTriggerProfileSchedule ( t )
installs , removes = checkNextPayloads ( t , mdmDevice2 , false )
assert . Len ( t , installs , 3 )
assert . Empty ( t , removes )
// Add a profile again
2025-04-30 20:03:23 +00:00
s . Do ( "POST" , "/api/latest/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : "N1" , Contents : globalProfilesPlusOne [ 0 ] } ,
{ Name : "N2" , Contents : globalProfilesPlusOne [ 1 ] } ,
} } , http . StatusNoContent )
2025-01-06 19:16:34 +00:00
// trigger a profile sync
s . awaitTriggerProfileSchedule ( t )
// Delete a profile before it is sent to both devices
s . Do ( "POST" , "/api/v1/fleet/mdm/apple/profiles/batch" , batchSetMDMAppleProfilesRequest { Profiles : globalProfiles } , http . StatusNoContent )
// trigger a profile sync
s . awaitTriggerProfileSchedule ( t )
// The host grabs the removal command from Fleet
sendErrorOnRemoveProfile ( mdmDevice )
sendErrorOnRemoveProfile ( mdmDevice2 )
// Make sure deleted profile no longer shows up on either host
profiles , err = s . ds . GetHostMDMAppleProfiles ( ctx , host . UUID )
require . NoError ( t , err )
assert . Len ( t , profiles , 3 )
profiles , err = s . ds . GetHostMDMAppleProfiles ( ctx , host2 . UUID )
require . NoError ( t , err )
assert . Len ( t , profiles , 3 )
}
2025-05-07 20:48:18 +00:00
func ( s * integrationMDMTestSuite ) TestBatchResendMDMProfiles ( ) {
t := s . T ( )
2025-05-13 12:49:08 +00:00
ctx := t . Context ( )
2025-05-07 20:48:18 +00:00
s . setSkipWorkerJobs ( t )
// create a few hosts
host1 , _ := createHostThenEnrollMDM ( s . ds , s . server . URL , t )
host2 , _ := createHostThenEnrollMDM ( s . ds , s . server . URL , t )
host3 , _ := createWindowsHostThenEnrollMDM ( s . ds , s . server . URL , t )
// register a couple profiles for Apple and one for Windows
profN1 := mobileconfigForTest ( "N1" , "I1" )
profN2 := mobileconfigForTest ( "N2" , "I2" )
profN3 := syncMLForTest ( "./Foo/N3" )
declN4 := declarationForTest ( "N4" )
batchRequest := batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : "N1" , Contents : profN1 } ,
{ Name : "N2" , Contents : profN2 } ,
{ Name : "N3" , Contents : profN3 } ,
{ Name : "N4" , Contents : declN4 } ,
} }
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchRequest , http . StatusNoContent )
// list the profiles to get the UUIDs
var listResp listMDMConfigProfilesResponse
s . DoJSON ( "GET" , "/api/latest/fleet/configuration_profiles" , nil , http . StatusOK , & listResp )
profNameToPayload := make ( map [ string ] * fleet . MDMConfigProfilePayload )
for _ , prof := range listResp . Profiles {
2025-05-20 12:55:51 +00:00
if len ( prof . Checksum ) == 0 {
// not important, but must not be empty or it causes issues when forcing a status
prof . Checksum = [ ] byte ( "checksum" )
}
2025-05-07 20:48:18 +00:00
profNameToPayload [ prof . Name ] = prof
}
2025-05-13 12:49:08 +00:00
// get status for non-existing profile
s . Do ( "GET" , fmt . Sprintf ( "/api/v1/fleet/configuration_profiles/%s/status" , "ano-such-profile" ) , nil , http . StatusNotFound )
s . Do ( "GET" , fmt . Sprintf ( "/api/v1/fleet/configuration_profiles/%s/status" , "wno-such-profile" ) , nil , http . StatusNotFound )
s . Do ( "GET" , fmt . Sprintf ( "/api/v1/fleet/configuration_profiles/%s/status" , "dno-such-profile" ) , nil , http . StatusNotFound )
s . Do ( "GET" , fmt . Sprintf ( "/api/v1/fleet/configuration_profiles/%s/status" , "zno-such-profile" ) , nil , http . StatusNotFound )
// get status for existing profiles, all 0 counts
for _ , uuid := range [ ] string { profNameToPayload [ "N1" ] . ProfileUUID , profNameToPayload [ "N2" ] . ProfileUUID , profNameToPayload [ "N3" ] . ProfileUUID } {
var statusResp getMDMConfigProfileStatusResponse
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/v1/fleet/configuration_profiles/%s/status" , uuid ) , getMDMConfigProfileStatusRequest { } , http . StatusOK , & statusResp )
require . Equal ( t , fleet . MDMConfigProfileStatus { } , statusResp . MDMConfigProfileStatus )
}
// except for the declaration, which is immediately set as pending on the hosts
var statusResp getMDMConfigProfileStatusResponse
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/v1/fleet/configuration_profiles/%s/status" , profNameToPayload [ "N4" ] . ProfileUUID ) , getMDMConfigProfileStatusRequest { } , http . StatusOK , & statusResp )
require . Equal ( t , fleet . MDMConfigProfileStatus { Pending : 2 } , statusResp . MDMConfigProfileStatus )
2025-05-07 20:48:18 +00:00
// try to batch-resend a non-existing profile
batchReq := batchResendMDMProfileToHostsRequest { ProfileUUID : "zzzz" } // not a known prefix
batchReq . Filters . ProfileStatus = string ( fleet . MDMDeliveryFailed )
s . Do ( "POST" , "/api/v1/fleet/configuration_profiles/resend/batch" , batchReq , http . StatusNotFound )
batchReq = batchResendMDMProfileToHostsRequest { ProfileUUID : "azzzz" } // unknown Apple profile
batchReq . Filters . ProfileStatus = string ( fleet . MDMDeliveryFailed )
s . Do ( "POST" , "/api/v1/fleet/configuration_profiles/resend/batch" , batchReq , http . StatusNotFound )
batchReq = batchResendMDMProfileToHostsRequest { ProfileUUID : "wzzzz" } // unknown Windows profile
batchReq . Filters . ProfileStatus = string ( fleet . MDMDeliveryFailed )
s . Do ( "POST" , "/api/v1/fleet/configuration_profiles/resend/batch" , batchReq , http . StatusNotFound )
// batch-resend with an invalid filter
batchReq = batchResendMDMProfileToHostsRequest { ProfileUUID : profNameToPayload [ "N1" ] . ProfileUUID }
batchReq . Filters . ProfileStatus = string ( fleet . MDMDeliveryPending )
res := s . Do ( "POST" , "/api/v1/fleet/configuration_profiles/resend/batch" , batchReq , http . StatusBadRequest )
msg := extractServerErrorText ( res . Body )
require . Contains ( t , msg , "Invalid profile_status filter value, only 'failed' is currently supported." )
// batch-resend with an Apple DDM
batchReq = batchResendMDMProfileToHostsRequest { ProfileUUID : profNameToPayload [ "N4" ] . ProfileUUID }
batchReq . Filters . ProfileStatus = string ( fleet . MDMDeliveryFailed )
res = s . Do ( "POST" , "/api/v1/fleet/configuration_profiles/resend/batch" , batchReq , http . StatusBadRequest )
msg = extractServerErrorText ( res . Body )
require . Contains ( t , msg , "Can't resend declaration (DDM) profiles." )
// batch-resend an Apple and a Windows profile, does nothing as it is not delivered yet
batchReq = batchResendMDMProfileToHostsRequest { ProfileUUID : profNameToPayload [ "N1" ] . ProfileUUID }
batchReq . Filters . ProfileStatus = string ( fleet . MDMDeliveryFailed )
s . Do ( "POST" , "/api/v1/fleet/configuration_profiles/resend/batch" , batchReq , http . StatusAccepted )
batchReq = batchResendMDMProfileToHostsRequest { ProfileUUID : profNameToPayload [ "N3" ] . ProfileUUID }
batchReq . Filters . ProfileStatus = string ( fleet . MDMDeliveryFailed )
s . Do ( "POST" , "/api/v1/fleet/configuration_profiles/resend/batch" , batchReq , http . StatusAccepted )
2025-05-20 12:55:51 +00:00
forceSetAppleHostProfileStatus ( t , s . ds , host1 . UUID , test . ToMDMAppleConfigProfile ( profNameToPayload [ "N1" ] ) , fleet . MDMOperationTypeInstall , fleet . MDMDeliveryPending )
forceSetAppleHostProfileStatus ( t , s . ds , host1 . UUID , test . ToMDMAppleConfigProfile ( profNameToPayload [ "N2" ] ) , fleet . MDMOperationTypeInstall , fleet . MDMDeliveryPending )
forceSetAppleHostProfileStatus ( t , s . ds , host2 . UUID , test . ToMDMAppleConfigProfile ( profNameToPayload [ "N1" ] ) , fleet . MDMOperationTypeInstall , fleet . MDMDeliveryPending )
forceSetAppleHostProfileStatus ( t , s . ds , host2 . UUID , test . ToMDMAppleConfigProfile ( profNameToPayload [ "N2" ] ) , fleet . MDMOperationTypeInstall , fleet . MDMDeliveryPending )
forceSetWindowsHostProfileStatus ( t , s . ds , host3 . UUID , test . ToMDMWindowsConfigProfile ( profNameToPayload [ "N3" ] ) , fleet . MDMOperationTypeInstall , fleet . MDMDeliveryPending )
2025-05-07 20:48:18 +00:00
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
host1 : {
{ Identifier : "I1" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "I2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "N4" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
} ,
host2 : {
{ Identifier : "I1" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "I2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "N4" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
} ,
} )
s . assertHostWindowsConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMWindowsProfile {
host3 : {
{ Name : "N3" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
} ,
} )
2025-05-13 12:49:08 +00:00
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/v1/fleet/configuration_profiles/%s/status" , profNameToPayload [ "N1" ] . ProfileUUID ) , getMDMConfigProfileStatusRequest { } , http . StatusOK , & statusResp )
require . Equal ( t , fleet . MDMConfigProfileStatus { Pending : 2 } , statusResp . MDMConfigProfileStatus )
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/v1/fleet/configuration_profiles/%s/status" , profNameToPayload [ "N2" ] . ProfileUUID ) , getMDMConfigProfileStatusRequest { } , http . StatusOK , & statusResp )
require . Equal ( t , fleet . MDMConfigProfileStatus { Pending : 2 } , statusResp . MDMConfigProfileStatus )
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/v1/fleet/configuration_profiles/%s/status" , profNameToPayload [ "N3" ] . ProfileUUID ) , getMDMConfigProfileStatusRequest { } , http . StatusOK , & statusResp )
require . Equal ( t , fleet . MDMConfigProfileStatus { Pending : 1 } , statusResp . MDMConfigProfileStatus )
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/v1/fleet/configuration_profiles/%s/status" , profNameToPayload [ "N4" ] . ProfileUUID ) , getMDMConfigProfileStatusRequest { } , http . StatusOK , & statusResp )
require . Equal ( t , fleet . MDMConfigProfileStatus { Pending : 2 } , statusResp . MDMConfigProfileStatus )
2025-05-07 20:48:18 +00:00
// acknowledge the Apple profiles, failing I2 on both hosts, and fail the Windows one
2025-05-20 12:55:51 +00:00
forceSetAppleHostProfileStatus ( t , s . ds , host1 . UUID , test . ToMDMAppleConfigProfile ( profNameToPayload [ "N1" ] ) , fleet . MDMOperationTypeInstall , fleet . MDMDeliveryVerifying )
forceSetAppleHostProfileStatus ( t , s . ds , host1 . UUID , test . ToMDMAppleConfigProfile ( profNameToPayload [ "N2" ] ) , fleet . MDMOperationTypeInstall , fleet . MDMDeliveryFailed )
forceSetAppleHostProfileStatus ( t , s . ds , host2 . UUID , test . ToMDMAppleConfigProfile ( profNameToPayload [ "N1" ] ) , fleet . MDMOperationTypeInstall , fleet . MDMDeliveryVerifying )
forceSetAppleHostProfileStatus ( t , s . ds , host2 . UUID , test . ToMDMAppleConfigProfile ( profNameToPayload [ "N2" ] ) , fleet . MDMOperationTypeInstall , fleet . MDMDeliveryFailed )
forceSetWindowsHostProfileStatus ( t , s . ds , host3 . UUID , test . ToMDMWindowsConfigProfile ( profNameToPayload [ "N3" ] ) , fleet . MDMOperationTypeInstall , fleet . MDMDeliveryFailed )
2025-05-07 20:48:18 +00:00
// batch-resend N2 profile
batchReq = batchResendMDMProfileToHostsRequest { ProfileUUID : profNameToPayload [ "N2" ] . ProfileUUID }
batchReq . Filters . ProfileStatus = string ( fleet . MDMDeliveryFailed )
s . Do ( "POST" , "/api/v1/fleet/configuration_profiles/resend/batch" , batchReq , http . StatusAccepted )
s . lastActivityOfTypeMatches (
fleet . ActivityTypeResentConfigurationProfileBatch { } . ActivityName ( ) ,
fmt . Sprintf ( ` { "profile_name": %q, "host_count": %d} ` , "N2" , 2 ) ,
0 ,
)
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
host1 : {
{ Identifier : "I1" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : "I2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "N4" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
} ,
host2 : {
{ Identifier : "I1" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : "I2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "N4" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
} ,
} )
s . assertHostWindowsConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMWindowsProfile {
host3 : {
{ Name : "N3" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryFailed } ,
} ,
} )
// set I2/N2 as verifying
2025-05-20 12:55:51 +00:00
forceSetAppleHostProfileStatus ( t , s . ds , host1 . UUID , test . ToMDMAppleConfigProfile ( profNameToPayload [ "N2" ] ) , fleet . MDMOperationTypeInstall , fleet . MDMDeliveryVerifying )
forceSetAppleHostProfileStatus ( t , s . ds , host2 . UUID , test . ToMDMAppleConfigProfile ( profNameToPayload [ "N2" ] ) , fleet . MDMOperationTypeInstall , fleet . MDMDeliveryVerifying )
2025-05-07 20:48:18 +00:00
// batch-resend N3 profile
batchReq = batchResendMDMProfileToHostsRequest { ProfileUUID : profNameToPayload [ "N3" ] . ProfileUUID }
batchReq . Filters . ProfileStatus = string ( fleet . MDMDeliveryFailed )
s . Do ( "POST" , "/api/v1/fleet/configuration_profiles/resend/batch" , batchReq , http . StatusAccepted )
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
host1 : {
{ Identifier : "I1" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : "I2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : "N4" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
} ,
host2 : {
{ Identifier : "I1" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : "I2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryVerifying } ,
{ Identifier : "N4" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
} ,
} )
s . assertHostWindowsConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMWindowsProfile {
host3 : {
{ Name : "N3" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
} ,
} )
s . lastActivityOfTypeMatches (
fleet . ActivityTypeResentConfigurationProfileBatch { } . ActivityName ( ) ,
fmt . Sprintf ( ` { "profile_name": %q, "host_count": %d} ` , "N3" , 1 ) ,
0 ,
)
2025-05-13 12:49:08 +00:00
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/v1/fleet/configuration_profiles/%s/status" , profNameToPayload [ "N1" ] . ProfileUUID ) , getMDMConfigProfileStatusRequest { } , http . StatusOK , & statusResp )
require . Equal ( t , fleet . MDMConfigProfileStatus { Verifying : 2 } , statusResp . MDMConfigProfileStatus )
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/v1/fleet/configuration_profiles/%s/status" , profNameToPayload [ "N2" ] . ProfileUUID ) , getMDMConfigProfileStatusRequest { } , http . StatusOK , & statusResp )
require . Equal ( t , fleet . MDMConfigProfileStatus { Verifying : 2 } , statusResp . MDMConfigProfileStatus )
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/v1/fleet/configuration_profiles/%s/status" , profNameToPayload [ "N3" ] . ProfileUUID ) , getMDMConfigProfileStatusRequest { } , http . StatusOK , & statusResp )
require . Equal ( t , fleet . MDMConfigProfileStatus { Pending : 1 } , statusResp . MDMConfigProfileStatus )
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/v1/fleet/configuration_profiles/%s/status" , profNameToPayload [ "N4" ] . ProfileUUID ) , getMDMConfigProfileStatusRequest { } , http . StatusOK , & statusResp )
require . Equal ( t , fleet . MDMConfigProfileStatus { Pending : 2 } , statusResp . MDMConfigProfileStatus )
// trigger profile schedule to get the fleet-controlled profiles
s . awaitTriggerProfileSchedule ( t )
// list the profiles to get a fleet-controlled profile UUID
gotProfs , err := s . ds . GetHostMDMAppleProfiles ( ctx , host1 . UUID )
require . NoError ( t , err )
var fleetReservedProfile string
for _ , prof := range gotProfs {
// find the fleetd config one
if prof . Identifier == mobileconfig . FleetdConfigPayloadIdentifier {
fleetReservedProfile = prof . ProfileUUID
}
}
require . NotEmpty ( t , fleetReservedProfile )
// fleet-reserved profiles are not returned by the API, only custom profiles
s . Do ( "GET" , fmt . Sprintf ( "/api/v1/fleet/configuration_profiles/%s/status" , fleetReservedProfile ) , getMDMConfigProfileStatusRequest { } , http . StatusNotFound )
2025-05-07 20:48:18 +00:00
}
2025-05-20 12:55:51 +00:00
func ( s * integrationMDMTestSuite ) TestDeleteMDMProfileCancelsInstalls ( ) {
t := s . T ( )
s . setSkipWorkerJobs ( t )
// create some Apple, Windows and declaration profiles
profiles := [ ] fleet . MDMProfileBatchPayload {
{
Name : "A1" ,
Contents : mobileconfigForTest ( "A1" , "A1" ) ,
} ,
{
Name : "A2" ,
Contents : mobileconfigForTest ( "A2" , "A2" ) ,
} ,
{
Name : "D1" ,
Contents : declarationForTest ( "D1" ) ,
} ,
{
Name : "D2" ,
Contents : declarationForTest ( "D2" ) ,
} ,
{
Name : "W1" ,
Contents : syncml . ForTestWithData ( [ ] syncml . TestCommand { { Verb : "Replace" , LocURI : "W1" , Data : "W1" } } ) ,
} ,
{
Name : "W2" ,
Contents : syncml . ForTestWithData ( [ ] syncml . TestCommand { { Verb : "Replace" , LocURI : "W2" , Data : "W2" } } ) ,
} ,
}
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : profiles } , http . StatusNoContent )
// list the profiles to get the UUIDs
var listResp listMDMConfigProfilesResponse
s . DoJSON ( "GET" , "/api/latest/fleet/configuration_profiles" , nil , http . StatusOK , & listResp )
profNameToPayload := make ( map [ string ] * fleet . MDMConfigProfilePayload )
for _ , prof := range listResp . Profiles {
if len ( prof . Checksum ) == 0 {
// not important, but must not be empty or it causes issues when forcing a status
prof . Checksum = [ ] byte ( "checksum" )
}
profNameToPayload [ prof . Name ] = prof
t . Logf ( "profile %s: %s" , prof . Name , prof . ProfileUUID )
}
// deleting without any affected host is fine
var deleteResp deleteMDMConfigProfileResponse
s . DoJSON ( "DELETE" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , profNameToPayload [ "A1" ] . ProfileUUID ) , nil , http . StatusOK , & deleteResp )
s . DoJSON ( "DELETE" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , profNameToPayload [ "D1" ] . ProfileUUID ) , nil , http . StatusOK , & deleteResp )
s . DoJSON ( "DELETE" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , profNameToPayload [ "W1" ] . ProfileUUID ) , nil , http . StatusOK , & deleteResp )
// create some Apple and Windows hosts
host1 , _ := createHostThenEnrollMDM ( s . ds , s . server . URL , t )
host2 , _ := createHostThenEnrollMDM ( s . ds , s . server . URL , t )
host3 , _ := createWindowsHostThenEnrollMDM ( s . ds , s . server . URL , t )
host4 , _ := createWindowsHostThenEnrollMDM ( s . ds , s . server . URL , t )
for i , h := range [ ] * fleet . Host { host1 , host2 , host3 , host4 } {
t . Logf ( "host %d: %s" , i + 1 , h . UUID )
}
s . awaitTriggerProfileSchedule ( t )
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
host1 : {
{ Identifier : "A2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "D2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
} ,
host2 : {
{ Identifier : "A2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "D2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
} ,
} )
s . assertHostWindowsConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMWindowsProfile {
host3 : {
{ Name : "W2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
} ,
host4 : {
{ Name : "W2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
} ,
} )
// for the declaration, set host1 as NULL and host2 as verified
forceSetAppleHostDeclarationStatus ( t , s . ds , host1 . UUID , test . ToMDMAppleDecl ( profNameToPayload [ "D2" ] ) , fleet . MDMOperationTypeInstall , "" )
forceSetAppleHostDeclarationStatus ( t , s . ds , host2 . UUID , test . ToMDMAppleDecl ( profNameToPayload [ "D2" ] ) , fleet . MDMOperationTypeInstall , fleet . MDMDeliveryVerified )
// delete the declaration, will have removed it for host1 and set to remove pending for host2
s . DoJSON ( "DELETE" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , profNameToPayload [ "D2" ] . ProfileUUID ) , nil , http . StatusOK , & deleteResp )
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
host1 : {
{ Identifier : "A2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
} ,
host2 : {
{ Identifier : "A2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "D2" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
} ,
} )
// for the Windows profile, set host4 as failed
forceSetWindowsHostProfileStatus ( t , s . ds , host4 . UUID , test . ToMDMWindowsConfigProfile ( profNameToPayload [ "W2" ] ) , fleet . MDMOperationTypeInstall , fleet . MDMDeliveryFailed )
// delete the Windows profile, will have removed it for both (because there
// is no "Remove profile" for now with Windows)
s . DoJSON ( "DELETE" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , profNameToPayload [ "W2" ] . ProfileUUID ) , nil , http . StatusOK , & deleteResp )
s . assertHostWindowsConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMWindowsProfile {
host3 : { } ,
host4 : { } ,
} )
// for the Apple profile, set host1 as NULL (pending not queued yet), and leave host2 as actually pending (queued)
forceSetAppleHostProfileStatus ( t , s . ds , host1 . UUID , test . ToMDMAppleConfigProfile ( profNameToPayload [ "A2" ] ) , fleet . MDMOperationTypeInstall , "" )
assertIsCommandActiveForHostAndProfile := func ( hostUUID , profileUUID string , wantActive bool ) {
var active bool
ctx := t . Context ( )
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
return sqlx . GetContext ( ctx , q , & active , ` SELECT neq . active
FROM
nano_enrollment_queue neq
JOIN host_mdm_apple_profiles hmap
ON hmap . command_uuid = neq . command_uuid AND hmap . host_uuid = neq . id
WHERE
hmap . host_uuid = ? AND
hmap . profile_uuid = ? ` , hostUUID , profileUUID )
} )
if wantActive {
require . True ( t , active )
} else {
require . False ( t , active )
}
}
assertIsCommandActiveForHostAndProfile ( host1 . UUID , profNameToPayload [ "A2" ] . ProfileUUID , true )
assertIsCommandActiveForHostAndProfile ( host2 . UUID , profNameToPayload [ "A2" ] . ProfileUUID , true )
// delete the profile, will remove the row for host1 and set host2 to pending remove (and will deactivate the associated nano command)
s . DoJSON ( "DELETE" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , profNameToPayload [ "A2" ] . ProfileUUID ) , nil , http . StatusOK , & deleteResp )
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
host1 : {
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
} ,
host2 : {
{ Identifier : "A2" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "D2" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
} ,
} )
assertIsCommandActiveForHostAndProfile ( host2 . UUID , profNameToPayload [ "A2" ] . ProfileUUID , false )
// set the remove operations to verifying and reconcile profiles
forceSetAppleHostProfileStatus ( t , s . ds , host2 . UUID , test . ToMDMAppleConfigProfile ( profNameToPayload [ "A2" ] ) , fleet . MDMOperationTypeRemove , fleet . MDMDeliveryVerifying )
forceSetAppleHostDeclarationStatus ( t , s . ds , host2 . UUID , test . ToMDMAppleDecl ( profNameToPayload [ "D2" ] ) , fleet . MDMOperationTypeRemove , fleet . MDMDeliveryVerifying )
s . awaitTriggerProfileSchedule ( t )
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
host1 : {
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
} ,
host2 : {
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
} ,
} )
s . assertHostWindowsConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMWindowsProfile {
host3 : { } ,
host4 : { } ,
} )
// add new profile A3 and re-add A2, behaves as if a new profile because it has a new uuid
oldA2Contents := profiles [ 1 ] . Contents
profiles = [ ] fleet . MDMProfileBatchPayload {
{
Name : "A2" ,
Contents : oldA2Contents ,
} ,
{
Name : "A3" ,
Contents : mobileconfigForTest ( "A3" , "A3" ) ,
} ,
}
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : profiles } , http . StatusNoContent )
// list the profiles to get the UUIDs
s . DoJSON ( "GET" , "/api/latest/fleet/configuration_profiles" , nil , http . StatusOK , & listResp )
profNameToPayload = make ( map [ string ] * fleet . MDMConfigProfilePayload )
for _ , prof := range listResp . Profiles {
if len ( prof . Checksum ) == 0 {
// not important, but must not be empty or it causes issues when forcing a status
prof . Checksum = [ ] byte ( "checksum" )
}
profNameToPayload [ prof . Name ] = prof
t . Logf ( "new profile %s: %s" , prof . Name , prof . ProfileUUID )
}
s . awaitTriggerProfileSchedule ( t )
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
host1 : {
{ Identifier : "A2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "A3" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
} ,
host2 : {
{ Identifier : "A2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "A3" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
} ,
} )
// set A3 as failed on host1, and removed on host2
forceSetAppleHostProfileStatus ( t , s . ds , host1 . UUID , test . ToMDMAppleConfigProfile ( profNameToPayload [ "A3" ] ) , fleet . MDMOperationTypeInstall , fleet . MDMDeliveryFailed )
forceSetAppleHostProfileStatus ( t , s . ds , host2 . UUID , test . ToMDMAppleConfigProfile ( profNameToPayload [ "A3" ] ) , fleet . MDMOperationTypeRemove , fleet . MDMDeliveryFailed )
// delete the profile, will mark host1 as pending remove and will not touch host2 (not installed)
s . DoJSON ( "DELETE" , fmt . Sprintf ( "/api/latest/fleet/configuration_profiles/%s" , profNameToPayload [ "A3" ] . ProfileUUID ) , nil , http . StatusOK , & deleteResp )
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
host1 : {
{ Identifier : "A2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "A3" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
} ,
host2 : {
{ Identifier : "A2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "A3" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryFailed } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
} ,
} )
forceSetAppleHostProfileStatus ( t , s . ds , host1 . UUID , test . ToMDMAppleConfigProfile ( profNameToPayload [ "A3" ] ) , fleet . MDMOperationTypeRemove , fleet . MDMDeliveryVerifying )
s . awaitTriggerProfileSchedule ( t )
s . assertHostAppleConfigProfiles ( map [ * fleet . Host ] [ ] fleet . HostMDMAppleProfile {
host1 : {
{ Identifier : "A2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
} ,
host2 : {
{ Identifier : "A2" , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : "A3" , OperationType : fleet . MDMOperationTypeRemove , Status : & fleet . MDMDeliveryFailed } ,
{ Identifier : mobileconfig . FleetdConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
{ Identifier : mobileconfig . FleetCARootConfigPayloadIdentifier , OperationType : fleet . MDMOperationTypeInstall , Status : & fleet . MDMDeliveryPending } ,
} ,
} )
}
// those helper functions to force-set a host profile status are copied from the Datastore
// tests, couldn't put them in the test package due to circular dependency with mysql, would
// be nice to find a way to avoid this copy eventually.
func forceSetAppleHostProfileStatus ( t * testing . T , ds * mysql . Datastore , hostUUID string , profile * fleet . MDMAppleConfigProfile , operation fleet . MDMOperationType , status fleet . MDMDeliveryStatus ) {
ctx := t . Context ( )
// empty status string means set to NULL
var actualStatus * fleet . MDMDeliveryStatus
if status != "" {
actualStatus = & status
}
mysql . ExecAdhocSQL ( t , ds , func ( q sqlx . ExtContext ) error {
_ , err := q . ExecContext ( ctx , ` INSERT INTO host_mdm_apple_profiles
( profile_identifier , host_uuid , status , operation_type , command_uuid , profile_name , checksum , profile_uuid )
VALUES
( ? , ? , ? , ? , ? , ? , UNHEX ( MD5 ( ? ) ) , ? )
ON DUPLICATE KEY UPDATE
status = VALUES ( status ) ,
operation_type = VALUES ( operation_type )
` ,
profile . Identifier , hostUUID , actualStatus , operation , uuid . NewString ( ) , profile . Name , profile . Mobileconfig , profile . ProfileUUID )
return err
} )
}
func forceSetWindowsHostProfileStatus ( t * testing . T , ds * mysql . Datastore , hostUUID string , profile * fleet . MDMWindowsConfigProfile , operation fleet . MDMOperationType , status fleet . MDMDeliveryStatus ) {
ctx := t . Context ( )
// empty status string means set to NULL
var actualStatus * fleet . MDMDeliveryStatus
if status != "" {
actualStatus = & status
}
mysql . ExecAdhocSQL ( t , ds , func ( q sqlx . ExtContext ) error {
_ , err := q . ExecContext ( ctx , ` INSERT INTO host_mdm_windows_profiles
( host_uuid , status , operation_type , command_uuid , profile_name , checksum , profile_uuid )
VALUES
( ? , ? , ? , ? , ? , UNHEX ( MD5 ( ? ) ) , ? )
ON DUPLICATE KEY UPDATE
status = VALUES ( status ) ,
operation_type = VALUES ( operation_type )
` ,
hostUUID , actualStatus , operation , uuid . NewString ( ) , profile . Name , profile . SyncML , profile . ProfileUUID )
return err
} )
}
func forceSetAppleHostDeclarationStatus ( t * testing . T , ds * mysql . Datastore , hostUUID string , profile * fleet . MDMAppleDeclaration , operation fleet . MDMOperationType , status fleet . MDMDeliveryStatus ) {
ctx := t . Context ( )
// empty status string means set to NULL
var actualStatus * fleet . MDMDeliveryStatus
if status != "" {
actualStatus = & status
}
mysql . ExecAdhocSQL ( t , ds , func ( q sqlx . ExtContext ) error {
_ , err := q . ExecContext ( ctx , ` INSERT INTO host_mdm_apple_declarations
( declaration_identifier , host_uuid , status , operation_type , token , declaration_name , declaration_uuid )
VALUES
( ? , ? , ? , ? , ? , ? , ? )
ON DUPLICATE KEY UPDATE
status = VALUES ( status ) ,
operation_type = VALUES ( operation_type )
` ,
2025-08-03 06:18:13 +00:00
profile . Identifier , hostUUID , actualStatus , operation , test . MakeTestBytes ( ) , profile . Name , profile . DeclarationUUID )
2025-05-20 12:55:51 +00:00
return err
} )
}
2025-06-25 13:51:43 +00:00
func ( s * integrationMDMTestSuite ) TestVerifyUserScopedProfiles ( ) {
t := s . T ( )
ctx := t . Context ( )
s . setSkipWorkerJobs ( t )
// create a macOS host, will enroll only with device
host , device := createHostThenEnrollMDM ( s . ds , s . server . URL , t )
// create some profiles, system- and user-scoped
payloadScopeSystem := fleet . PayloadScopeSystem
payloadScopeUser := fleet . PayloadScopeUser
profiles := [ ] fleet . MDMProfileBatchPayload {
{ Name : "A1" , Contents : scopedMobileconfigForTest ( "A1" , "A1" , & payloadScopeSystem ) } ,
2025-07-02 14:54:54 +00:00
{ Name : "A2" , Contents : scopedMobileconfigForTest ( "A2" , "A2.user" , & payloadScopeUser ) } ,
{ Name : "A3" , Contents : scopedMobileconfigForTest ( "A3" , "A3.user" , & payloadScopeUser ) } ,
2025-06-25 13:51:43 +00:00
}
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : profiles } , http . StatusNoContent )
// ensure we are at least 1s after the profiles uploaded-at timestamp
time . Sleep ( time . Second )
// list the profiles to get the UUIDs
var listResp listMDMConfigProfilesResponse
s . DoJSON ( "GET" , "/api/latest/fleet/configuration_profiles" , nil , http . StatusOK , & listResp )
profNameToPayload := make ( map [ string ] * fleet . MDMConfigProfilePayload )
for _ , prof := range listResp . Profiles {
if len ( prof . Checksum ) == 0 {
// not important, but must not be empty or it causes issues when forcing a status
prof . Checksum = [ ] byte ( "checksum" )
}
profNameToPayload [ prof . Name ] = prof
t . Logf ( "profile %s: %s" , prof . Name , prof . ProfileUUID )
}
type hostProfile struct {
ProfileUUID string ` db:"profile_uuid" `
ProfileIdentifier string ` db:"profile_identifier" `
ProfileName string ` db:"profile_name" `
Status * string ` db:"status" `
OperationType * string ` db:"operation_type" `
Retries int ` db:"retries" `
Scope string ` db:"scope" `
}
assertHostProfiles := func ( want [ ] hostProfile ) {
var got [ ] hostProfile
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
// for the purpose of this test, we ignore the Fleet-internal profiles
// (we only care about the custom profiles)
return sqlx . SelectContext ( t . Context ( ) , q , & got , `
SELECT profile_uuid , profile_identifier , profile_name , status , operation_type , retries , scope
FROM host_mdm_apple_profiles
WHERE host_uuid = ? AND profile_identifier NOT IN ( ? , ? ) ` ,
host . UUID , mobileconfig . FleetdConfigPayloadIdentifier , mobileconfig . FleetCARootConfigPayloadIdentifier )
} )
require . ElementsMatch ( t , want , got )
}
forceProfileUploadeddAtTimestamp := func ( ident string , ts time . Time ) {
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
_ , err := q . ExecContext ( ctx , ` UPDATE mdm_apple_configuration_profiles
SET uploaded_at = ? WHERE identifier = ? ` , ts , ident )
return err
} )
}
// cron job hasn't run yet, so no profile exist for the host
assertHostProfiles ( [ ] hostProfile { } )
// trigger a profile sync
s . awaitTriggerProfileSchedule ( t )
2025-06-30 13:22:34 +00:00
// user-scoped profiles show up as status nil (no user-enrollment yet)
2025-06-25 13:51:43 +00:00
assertHostProfiles ( [ ] hostProfile {
{
ProfileUUID : profNameToPayload [ "A1" ] . ProfileUUID ,
ProfileIdentifier : profNameToPayload [ "A1" ] . Identifier ,
ProfileName : profNameToPayload [ "A1" ] . Name ,
Status : ptr . String ( string ( fleet . MDMDeliveryPending ) ) ,
OperationType : ptr . String ( string ( fleet . MDMOperationTypeInstall ) ) ,
Retries : 0 ,
Scope : string ( fleet . PayloadScopeSystem ) ,
} ,
2025-06-30 13:22:34 +00:00
{
ProfileUUID : profNameToPayload [ "A2" ] . ProfileUUID ,
ProfileIdentifier : profNameToPayload [ "A2" ] . Identifier ,
ProfileName : profNameToPayload [ "A2" ] . Name ,
Status : nil ,
OperationType : ptr . String ( string ( fleet . MDMOperationTypeInstall ) ) ,
Retries : 0 ,
Scope : string ( fleet . PayloadScopeUser ) ,
} ,
{
ProfileUUID : profNameToPayload [ "A3" ] . ProfileUUID ,
ProfileIdentifier : profNameToPayload [ "A3" ] . Identifier ,
ProfileName : profNameToPayload [ "A3" ] . Name ,
Status : nil ,
OperationType : ptr . String ( string ( fleet . MDMOperationTypeInstall ) ) ,
Retries : 0 ,
Scope : string ( fleet . PayloadScopeUser ) ,
} ,
2025-06-25 13:51:43 +00:00
} )
// verify the profiles, only the system one is reported as installed
host . DetailUpdatedAt = time . Now ( ) . UTC ( )
err := s . ds . UpdateHost ( ctx , host )
require . NoError ( t , err )
err = apple_mdm . VerifyHostMDMProfiles ( ctx , s . ds , host , map [ string ] * fleet . HostMacOSProfile {
profNameToPayload [ "A1" ] . Identifier : {
DisplayName : profNameToPayload [ "A1" ] . Name ,
Identifier : profNameToPayload [ "A1" ] . Identifier ,
InstallDate : time . Now ( ) . UTC ( ) ,
} ,
} )
require . NoError ( t , err )
// user-scoped profiles were left untouched
assertHostProfiles ( [ ] hostProfile {
{
ProfileUUID : profNameToPayload [ "A1" ] . ProfileUUID ,
ProfileIdentifier : profNameToPayload [ "A1" ] . Identifier ,
ProfileName : profNameToPayload [ "A1" ] . Name ,
Status : ptr . String ( string ( fleet . MDMDeliveryVerified ) ) ,
OperationType : ptr . String ( string ( fleet . MDMOperationTypeInstall ) ) ,
Retries : 0 ,
Scope : string ( fleet . PayloadScopeSystem ) ,
} ,
2025-06-30 13:22:34 +00:00
{
ProfileUUID : profNameToPayload [ "A2" ] . ProfileUUID ,
ProfileIdentifier : profNameToPayload [ "A2" ] . Identifier ,
ProfileName : profNameToPayload [ "A2" ] . Name ,
Status : nil ,
OperationType : ptr . String ( string ( fleet . MDMOperationTypeInstall ) ) ,
Retries : 0 ,
Scope : string ( fleet . PayloadScopeUser ) ,
} ,
{
ProfileUUID : profNameToPayload [ "A3" ] . ProfileUUID ,
ProfileIdentifier : profNameToPayload [ "A3" ] . Identifier ,
ProfileName : profNameToPayload [ "A3" ] . Name ,
Status : nil ,
OperationType : ptr . String ( string ( fleet . MDMOperationTypeInstall ) ) ,
Retries : 0 ,
Scope : string ( fleet . PayloadScopeUser ) ,
} ,
2025-06-25 13:51:43 +00:00
} )
// create the user-enrollment
err = device . UserEnroll ( )
require . NoError ( t , err )
// trigger a profile sync
s . awaitTriggerProfileSchedule ( t )
2025-06-30 13:22:34 +00:00
// user-scoped profiles have been added as pending (not nil)
2025-06-25 13:51:43 +00:00
assertHostProfiles ( [ ] hostProfile {
{
ProfileUUID : profNameToPayload [ "A1" ] . ProfileUUID ,
ProfileIdentifier : profNameToPayload [ "A1" ] . Identifier ,
ProfileName : profNameToPayload [ "A1" ] . Name ,
Status : ptr . String ( string ( fleet . MDMDeliveryVerified ) ) ,
OperationType : ptr . String ( string ( fleet . MDMOperationTypeInstall ) ) ,
Retries : 0 ,
Scope : string ( fleet . PayloadScopeSystem ) ,
} ,
{
ProfileUUID : profNameToPayload [ "A2" ] . ProfileUUID ,
ProfileIdentifier : profNameToPayload [ "A2" ] . Identifier ,
ProfileName : profNameToPayload [ "A2" ] . Name ,
Status : ptr . String ( string ( fleet . MDMDeliveryPending ) ) ,
OperationType : ptr . String ( string ( fleet . MDMOperationTypeInstall ) ) ,
Retries : 0 ,
Scope : string ( fleet . PayloadScopeUser ) ,
} ,
{
ProfileUUID : profNameToPayload [ "A3" ] . ProfileUUID ,
ProfileIdentifier : profNameToPayload [ "A3" ] . Identifier ,
ProfileName : profNameToPayload [ "A3" ] . Name ,
Status : ptr . String ( string ( fleet . MDMDeliveryPending ) ) ,
OperationType : ptr . String ( string ( fleet . MDMOperationTypeInstall ) ) ,
Retries : 0 ,
Scope : string ( fleet . PayloadScopeUser ) ,
} ,
} )
// verify the profiles, A3 is missing but still within the grace period
err = apple_mdm . VerifyHostMDMProfiles ( ctx , s . ds , host , map [ string ] * fleet . HostMacOSProfile {
profNameToPayload [ "A1" ] . Identifier : {
DisplayName : profNameToPayload [ "A1" ] . Name ,
Identifier : profNameToPayload [ "A1" ] . Identifier ,
InstallDate : time . Now ( ) . UTC ( ) ,
} ,
profNameToPayload [ "A2" ] . Identifier : {
DisplayName : profNameToPayload [ "A2" ] . Name ,
Identifier : profNameToPayload [ "A2" ] . Identifier ,
InstallDate : time . Now ( ) . UTC ( ) ,
} ,
} )
require . NoError ( t , err )
// A2 is now verified, A3 is still pending
assertHostProfiles ( [ ] hostProfile {
{
ProfileUUID : profNameToPayload [ "A1" ] . ProfileUUID ,
ProfileIdentifier : profNameToPayload [ "A1" ] . Identifier ,
ProfileName : profNameToPayload [ "A1" ] . Name ,
Status : ptr . String ( string ( fleet . MDMDeliveryVerified ) ) ,
OperationType : ptr . String ( string ( fleet . MDMOperationTypeInstall ) ) ,
Retries : 0 ,
Scope : string ( fleet . PayloadScopeSystem ) ,
} ,
{
ProfileUUID : profNameToPayload [ "A2" ] . ProfileUUID ,
ProfileIdentifier : profNameToPayload [ "A2" ] . Identifier ,
ProfileName : profNameToPayload [ "A2" ] . Name ,
Status : ptr . String ( string ( fleet . MDMDeliveryVerified ) ) ,
OperationType : ptr . String ( string ( fleet . MDMOperationTypeInstall ) ) ,
Retries : 0 ,
Scope : string ( fleet . PayloadScopeUser ) ,
} ,
{
ProfileUUID : profNameToPayload [ "A3" ] . ProfileUUID ,
ProfileIdentifier : profNameToPayload [ "A3" ] . Identifier ,
ProfileName : profNameToPayload [ "A3" ] . Name ,
Status : ptr . String ( string ( fleet . MDMDeliveryPending ) ) ,
OperationType : ptr . String ( string ( fleet . MDMOperationTypeInstall ) ) ,
Retries : 0 ,
Scope : string ( fleet . PayloadScopeUser ) ,
} ,
} )
// rewind the uploaded_at timestamp of A3 so it is not in the grace period
forceProfileUploadeddAtTimestamp ( profNameToPayload [ "A3" ] . Identifier , time . Now ( ) . Add ( - 24 * time . Hour ) )
// report as still missing
err = apple_mdm . VerifyHostMDMProfiles ( ctx , s . ds , host , map [ string ] * fleet . HostMacOSProfile {
profNameToPayload [ "A1" ] . Identifier : {
DisplayName : profNameToPayload [ "A1" ] . Name ,
Identifier : profNameToPayload [ "A1" ] . Identifier ,
InstallDate : time . Now ( ) . UTC ( ) ,
} ,
profNameToPayload [ "A2" ] . Identifier : {
DisplayName : profNameToPayload [ "A2" ] . Name ,
Identifier : profNameToPayload [ "A2" ] . Identifier ,
InstallDate : time . Now ( ) . UTC ( ) ,
} ,
} )
require . NoError ( t , err )
// A3 is now missing and retries
assertHostProfiles ( [ ] hostProfile {
{
ProfileUUID : profNameToPayload [ "A1" ] . ProfileUUID ,
ProfileIdentifier : profNameToPayload [ "A1" ] . Identifier ,
ProfileName : profNameToPayload [ "A1" ] . Name ,
Status : ptr . String ( string ( fleet . MDMDeliveryVerified ) ) ,
OperationType : ptr . String ( string ( fleet . MDMOperationTypeInstall ) ) ,
Retries : 0 ,
Scope : string ( fleet . PayloadScopeSystem ) ,
} ,
{
ProfileUUID : profNameToPayload [ "A2" ] . ProfileUUID ,
ProfileIdentifier : profNameToPayload [ "A2" ] . Identifier ,
ProfileName : profNameToPayload [ "A2" ] . Name ,
Status : ptr . String ( string ( fleet . MDMDeliveryVerified ) ) ,
OperationType : ptr . String ( string ( fleet . MDMOperationTypeInstall ) ) ,
Retries : 0 ,
Scope : string ( fleet . PayloadScopeUser ) ,
} ,
{
ProfileUUID : profNameToPayload [ "A3" ] . ProfileUUID ,
ProfileIdentifier : profNameToPayload [ "A3" ] . Identifier ,
ProfileName : profNameToPayload [ "A3" ] . Name ,
Status : nil ,
OperationType : ptr . String ( string ( fleet . MDMOperationTypeInstall ) ) ,
Retries : 1 ,
Scope : string ( fleet . PayloadScopeUser ) ,
} ,
} )
s . awaitTriggerProfileSchedule ( t )
// force-set it to Verifying so that by being missing again it goes to failed
// (it doesn't go to failed if it is pending)
forceSetAppleHostProfileStatus ( t , s . ds , host . UUID ,
test . ToMDMAppleConfigProfile ( profNameToPayload [ "A3" ] ) , fleet . MDMOperationTypeInstall , fleet . MDMDeliveryVerifying )
err = apple_mdm . VerifyHostMDMProfiles ( ctx , s . ds , host , map [ string ] * fleet . HostMacOSProfile {
profNameToPayload [ "A1" ] . Identifier : {
DisplayName : profNameToPayload [ "A1" ] . Name ,
Identifier : profNameToPayload [ "A1" ] . Identifier ,
InstallDate : time . Now ( ) . UTC ( ) ,
} ,
profNameToPayload [ "A2" ] . Identifier : {
DisplayName : profNameToPayload [ "A2" ] . Name ,
Identifier : profNameToPayload [ "A2" ] . Identifier ,
InstallDate : time . Now ( ) . UTC ( ) ,
} ,
} )
require . NoError ( t , err )
assertHostProfiles ( [ ] hostProfile {
{
ProfileUUID : profNameToPayload [ "A1" ] . ProfileUUID ,
ProfileIdentifier : profNameToPayload [ "A1" ] . Identifier ,
ProfileName : profNameToPayload [ "A1" ] . Name ,
Status : ptr . String ( string ( fleet . MDMDeliveryVerified ) ) ,
OperationType : ptr . String ( string ( fleet . MDMOperationTypeInstall ) ) ,
Retries : 0 ,
Scope : string ( fleet . PayloadScopeSystem ) ,
} ,
{
ProfileUUID : profNameToPayload [ "A2" ] . ProfileUUID ,
ProfileIdentifier : profNameToPayload [ "A2" ] . Identifier ,
ProfileName : profNameToPayload [ "A2" ] . Name ,
Status : ptr . String ( string ( fleet . MDMDeliveryVerified ) ) ,
OperationType : ptr . String ( string ( fleet . MDMOperationTypeInstall ) ) ,
Retries : 0 ,
Scope : string ( fleet . PayloadScopeUser ) ,
} ,
{
ProfileUUID : profNameToPayload [ "A3" ] . ProfileUUID ,
ProfileIdentifier : profNameToPayload [ "A3" ] . Identifier ,
ProfileName : profNameToPayload [ "A3" ] . Name ,
Status : ptr . String ( string ( fleet . MDMDeliveryFailed ) ) ,
OperationType : ptr . String ( string ( fleet . MDMOperationTypeInstall ) ) ,
Retries : 1 ,
Scope : string ( fleet . PayloadScopeUser ) ,
} ,
} )
}
2025-07-02 14:54:54 +00:00
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 ) )
}
2025-08-10 10:24:38 +00:00
func ( s * integrationMDMTestSuite ) TestWindowsProfilesWithFleetVariables ( ) {
t := s . T ( )
ctx := t . Context ( )
// Create a team for team-scoped tests
tm , err := s . ds . NewTeam ( ctx , & fleet . Team { Name : "test_windows_fleet_vars" } )
require . NoError ( t , err )
testCases := [ ] struct {
name string
profiles [ ] fleet . MDMProfileBatchPayload
teamID * uint
wantStatus int
wantErrContains string
} {
{
name : "HOST_UUID variable accepted for team" ,
profiles : [ ] fleet . MDMProfileBatchPayload {
{
Name : "TestHostUUID" ,
Contents : syncml . ForTestWithData ( [ ] syncml . TestCommand {
{ Verb : "Replace" , LocURI : "./Device/Vendor/MSFT/DMClient/Provider/ProviderID/UserSCEP_/SCEP/HostID" , Data : "$FLEET_VAR_HOST_UUID" } ,
} ) ,
} ,
} ,
teamID : & tm . ID ,
wantStatus : http . StatusNoContent ,
} ,
{
name : "HOST_UUID variable with braces accepted" ,
profiles : [ ] fleet . MDMProfileBatchPayload {
{
Name : "TestHostUUIDBraces" ,
Contents : syncml . ForTestWithData ( [ ] syncml . TestCommand {
{ Verb : "Replace" , LocURI : "./Device/Vendor/MSFT/DMClient/Provider/ProviderID/UserSCEP_/SCEP/HostID" , Data : "${FLEET_VAR_HOST_UUID}" } ,
} ) ,
} ,
} ,
teamID : & tm . ID ,
wantStatus : http . StatusNoContent ,
} ,
{
name : "unsupported variable rejected" ,
profiles : [ ] fleet . MDMProfileBatchPayload {
{
Name : "TestUnsupported" ,
Contents : syncml . ForTestWithData ( [ ] syncml . TestCommand {
2025-11-24 16:18:47 +00:00
{ Verb : "Replace" , LocURI : "./Device/Vendor/MSFT/DMClient/Provider/ProviderID/UserSCEP_/SCEP/Serial" , Data : "$FLEET_VAR_HOST_FAKE" } ,
2025-08-10 10:24:38 +00:00
} ) ,
} ,
} ,
teamID : & tm . ID ,
wantStatus : http . StatusUnprocessableEntity ,
2025-11-24 16:18:47 +00:00
wantErrContains : "Fleet variable $FLEET_VAR_HOST_FAKE is not supported in Windows profiles" ,
2025-08-10 10:24:38 +00:00
} ,
{
name : "mixed supported and unsupported variables rejected" ,
profiles : [ ] fleet . MDMProfileBatchPayload {
{
Name : "TestMixed" ,
Contents : syncml . ForTestWithData ( [ ] syncml . TestCommand {
{ Verb : "Replace" , LocURI : "./Device/Vendor/MSFT/DMClient/Provider/ProviderID/UserSCEP_/SCEP/HostID" , Data : "$FLEET_VAR_HOST_UUID" } ,
2025-10-27 13:48:37 +00:00
{ Verb : "Replace" , LocURI : "./Device/Vendor/MSFT/DMClient/Provider/ProviderID/UserSCEP_/SCEP/Email" , Data : "$FLEET_VAR_BOGUS" } ,
2025-08-10 10:24:38 +00:00
} ) ,
} ,
} ,
teamID : & tm . ID ,
wantStatus : http . StatusUnprocessableEntity ,
2025-10-27 13:48:37 +00:00
wantErrContains : "Fleet variable $FLEET_VAR_BOGUS is not supported in Windows profiles" ,
2025-08-10 10:24:38 +00:00
} ,
{
name : "HOST_UUID variable accepted globally" ,
profiles : [ ] fleet . MDMProfileBatchPayload {
{
Name : "GlobalHostUUID" ,
Contents : syncml . ForTestWithData ( [ ] syncml . TestCommand {
{ Verb : "Replace" , LocURI : "./Device/Vendor/MSFT/DMClient/Provider/ProviderID/UserSCEP_/SCEP/HostID" , Data : "$FLEET_VAR_HOST_UUID" } ,
} ) ,
} ,
} ,
teamID : nil , // global profile
wantStatus : http . StatusNoContent ,
} ,
{
name : "batch with regular and variable profiles accepted" ,
profiles : [ ] fleet . MDMProfileBatchPayload {
{
Name : "RegularProfile" ,
Contents : syncml . ForTestWithData ( [ ] syncml . TestCommand {
{ Verb : "Replace" , LocURI : "./Device/Vendor/MSFT/Policy/Config/System/AllowLocation" , Data : "1" } ,
} ) ,
} ,
{
Name : "ProfileWithVar" ,
Contents : syncml . ForTestWithData ( [ ] syncml . TestCommand {
{ Verb : "Replace" , LocURI : "./Device/Vendor/MSFT/DMClient/Provider/ProviderID/UserSCEP_/SCEP/HostID" , Data : "$FLEET_VAR_HOST_UUID" } ,
} ) ,
} ,
} ,
teamID : & tm . ID ,
wantStatus : http . StatusNoContent ,
} ,
{
name : "multiple HOST_UUID variables in single profile accepted" ,
profiles : [ ] fleet . MDMProfileBatchPayload {
{
Name : "MultipleHostUUID" ,
Contents : syncml . ForTestWithData ( [ ] syncml . TestCommand {
{ Verb : "Replace" , LocURI : "./Device/Vendor/MSFT/DMClient/Provider/ProviderID/UserSCEP_/SCEP/HostID" , Data : "$FLEET_VAR_HOST_UUID" } ,
{ Verb : "Replace" , LocURI : "./Device/Vendor/MSFT/DMClient/Provider/ProviderID/UserSCEP_/SCEP/BackupID" , Data : "${FLEET_VAR_HOST_UUID}" } ,
} ) ,
} ,
} ,
teamID : & tm . ID ,
wantStatus : http . StatusNoContent ,
} ,
{
name : "unknown Fleet variable rejected" ,
profiles : [ ] fleet . MDMProfileBatchPayload {
{
Name : "UnknownVar" ,
Contents : syncml . ForTestWithData ( [ ] syncml . TestCommand {
{ Verb : "Replace" , LocURI : "./Device/Vendor/MSFT/Policy/Config/System/SomeValue" , Data : "${FLEET_VAR_UNKNOWN_VAR}" } ,
} ) ,
} ,
} ,
teamID : & tm . ID ,
wantStatus : http . StatusUnprocessableEntity ,
wantErrContains : "Fleet variable $FLEET_VAR_UNKNOWN_VAR is not supported in Windows profiles" ,
} ,
}
for _ , tc := range testCases {
t . Run ( tc . name , func ( t * testing . T ) {
var resp * http . Response
// Execute request with or without team_id
if tc . teamID != nil {
resp = s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" ,
batchSetMDMProfilesRequest { Profiles : tc . profiles } ,
tc . wantStatus ,
"team_id" , fmt . Sprint ( * tc . teamID ) )
} else {
resp = s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" ,
batchSetMDMProfilesRequest { Profiles : tc . profiles } ,
tc . wantStatus )
}
// Check error message if expected
if tc . wantErrContains != "" {
errMsg := extractServerErrorText ( resp . Body )
require . Contains ( t , errMsg , tc . wantErrContains )
}
} )
}
}
func ( s * integrationMDMTestSuite ) TestWindowsProfilesFleetVariableSubstitution ( ) {
t := s . T ( )
ctx := context . Background ( )
// Create a team
tm , err := s . ds . NewTeam ( ctx , & fleet . Team { Name : t . Name ( ) + "team" } )
require . NoError ( t , err )
// Create and enroll three Windows hosts (two global, one in team)
hostGlobal1 , device1 := createWindowsHostThenEnrollMDM ( s . ds , s . server . URL , t )
hostGlobal2 , device2 := createWindowsHostThenEnrollMDM ( s . ds , s . server . URL , t )
hostTeam , deviceTeam := createWindowsHostThenEnrollMDM ( s . ds , s . server . URL , t )
// Add the team host to the team
err = s . ds . AddHostsToTeam ( ctx , fleet . NewAddHostsToTeamParams ( & tm . ID , [ ] uint { hostTeam . ID } ) )
require . NoError ( t , err )
// Create profiles with HOST_UUID variable for global and team
globalProfile := syncml . ForTestWithData ( [ ] syncml . TestCommand {
{ Verb : "Replace" , LocURI : "./Device/Vendor/MSFT/DMClient/Provider/ProviderID/UserSCEP_/SCEP/HostID" , Data : "Device ID: $FLEET_VAR_HOST_UUID" } ,
} )
teamProfile := syncml . ForTestWithData ( [ ] syncml . TestCommand {
{ Verb : "Replace" , LocURI : "./Device/Vendor/MSFT/DMClient/Provider/ProviderID/TeamDevice/ID" , Data : "Team Device: ${FLEET_VAR_HOST_UUID}" } ,
} )
// Upload global profile
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" ,
batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : "GlobalProfileWithVar" , Contents : globalProfile } ,
} } ,
http . StatusNoContent )
// Upload team profile
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" ,
batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : "TeamProfileWithVar" , Contents : teamProfile } ,
} } ,
http . StatusNoContent ,
"team_id" , fmt . Sprint ( tm . ID ) )
// Helper to verify profile contains substituted UUID
verifyProfileSubstitution := func ( device * mdmtest . TestWindowsMDMClient , expectedUUID string , expectedData string ) {
s . awaitTriggerProfileSchedule ( t )
cmds , err := device . StartManagementSession ( )
require . NoError ( t , err )
// Find the Atomic command containing the profile
var foundProfile bool
msgID , err := device . GetCurrentMsgID ( )
require . NoError ( t , err )
for _ , cmd := range cmds {
// Send status response for each command
device . AppendResponse ( fleet . SyncMLCmd {
XMLName : xml . Name { Local : fleet . CmdStatus } ,
MsgRef : & msgID ,
CmdRef : & cmd . Cmd . CmdID . Value ,
Cmd : ptr . String ( cmd . Verb ) ,
Data : ptr . String ( syncml . CmdStatusOK ) ,
CmdID : fleet . CmdID { Value : uuid . NewString ( ) } ,
} )
if cmd . Verb == "Atomic" {
// Check if the command contains our expected data with UUID substituted
for _ , replaceCmd := range cmd . Cmd . ReplaceCommands {
for _ , item := range replaceCmd . Items {
if item . Data != nil && item . Data . Content != "" {
if strings . Contains ( item . Data . Content , expectedData ) {
// Verify the UUID was substituted correctly
require . Contains ( t , item . Data . Content , expectedUUID )
require . NotContains ( t , item . Data . Content , "$FLEET_VAR_HOST_UUID" )
require . NotContains ( t , item . Data . Content , "${FLEET_VAR_HOST_UUID}" )
foundProfile = true
}
}
}
}
}
}
require . True ( t , foundProfile , "Expected profile with UUID substitution not found" )
// Send the response to complete the session
cmds , err = device . SendResponse ( )
require . NoError ( t , err )
// the ack of the message should be the only returned command
require . Len ( t , cmds , 1 )
}
// Verify global hosts receive profile with their UUID substituted
verifyProfileSubstitution ( device1 , hostGlobal1 . UUID , "Device ID: " + hostGlobal1 . UUID )
verifyProfileSubstitution ( device2 , hostGlobal2 . UUID , "Device ID: " + hostGlobal2 . UUID )
// Verify team host receives team profile with UUID substituted
verifyProfileSubstitution ( deviceTeam , hostTeam . UUID , "Team Device: " + hostTeam . UUID )
// Check that profile statuses are updated correctly in the database
2025-08-11 12:47:55 +00:00
checkHostProfileStatus := func ( hostUUID string , profileName string , expectedStatus fleet . MDMDeliveryStatus ) {
2025-08-10 10:24:38 +00:00
profiles , err := s . ds . GetHostMDMWindowsProfiles ( ctx , hostUUID )
require . NoError ( t , err )
2025-08-11 12:47:55 +00:00
// Find the specific profile by name
var foundProfile * fleet . HostMDMWindowsProfile
for _ , p := range profiles {
if p . Name == profileName {
foundProfile = & p
break
}
}
require . NotNil ( t , foundProfile , "Profile %s not found for host %s" , profileName , hostUUID )
require . NotNil ( t , foundProfile . Status , "Profile %s status is nil for host %s" , profileName , hostUUID )
assert . Equal ( t , expectedStatus , * foundProfile . Status , "Profile %s has unexpected status for host %s" , profileName , hostUUID )
}
checkHostProfileStatus ( hostGlobal1 . UUID , "GlobalProfileWithVar" , fleet . MDMDeliveryVerifying )
checkHostProfileStatus ( hostGlobal2 . UUID , "GlobalProfileWithVar" , fleet . MDMDeliveryVerifying )
checkHostProfileStatus ( hostTeam . UUID , "TeamProfileWithVar" , fleet . MDMDeliveryVerifying )
// Now let's check profile verification
// Also create and test a host without Fleet variables to ensure normal verification still works
hostNoVars , deviceNoVars := createWindowsHostThenEnrollMDM ( s . ds , s . server . URL , t )
// Create a profile without variables
profileNoVars := syncml . ForTestWithData ( [ ] syncml . TestCommand {
{ Verb : "Replace" , LocURI : "./Device/Vendor/MSFT/DMClient/Provider/ProviderID/Static/Value" , Data : "Static Value: NoSubstitution" } ,
} )
// Upload profile without variables
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" ,
batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : "ProfileNoVars" , Contents : profileNoVars } ,
} } ,
http . StatusNoContent )
// Let the host get the profile
s . awaitTriggerProfileSchedule ( t )
cmds , err := deviceNoVars . StartManagementSession ( )
require . NoError ( t , err )
msgID , err := deviceNoVars . GetCurrentMsgID ( )
require . NoError ( t , err )
for _ , cmd := range cmds {
deviceNoVars . AppendResponse ( fleet . SyncMLCmd {
XMLName : xml . Name { Local : fleet . CmdStatus } ,
MsgRef : & msgID ,
CmdRef : & cmd . Cmd . CmdID . Value ,
Cmd : ptr . String ( cmd . Verb ) ,
Data : ptr . String ( syncml . CmdStatusOK ) ,
CmdID : fleet . CmdID { Value : uuid . NewString ( ) } ,
} )
}
cmds , err = deviceNoVars . SendResponse ( )
require . NoError ( t , err )
require . Len ( t , cmds , 1 ) // ack
checkHostProfileStatus ( hostNoVars . UUID , "ProfileNoVars" , fleet . MDMDeliveryVerifying )
// To ensure any verification failures result in retry (pending) status instead of staying as verifying,
// we need to be outside the grace period. The grace period check is:
// hostDetailUpdatedAt.Before(profileEarliestInstallDate.Add(1 hour))
//
// We need to make sure the host checked in recently (detail_updated_at = now)
// but the profiles are old (created more than 1 hour ago)
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
// Set profile timestamps to 2 hours ago
// IMPORTANT: uploaded_at is used as EarliestInstallDate in verification logic
_ , err := q . ExecContext ( ctx ,
` UPDATE mdm_windows_configuration_profiles
SET created_at = DATE_SUB ( NOW ( ) , INTERVAL 2 HOUR ) ,
uploaded_at = DATE_SUB ( NOW ( ) , INTERVAL 2 HOUR )
WHERE name IN ( ? , ? , ? ) ` ,
"GlobalProfileWithVar" , "TeamProfileWithVar" , "ProfileNoVars" )
if err != nil {
return err
}
// Also update the host profile associations to have old timestamps (only created_at)
_ , err = q . ExecContext ( ctx ,
` UPDATE host_mdm_windows_profiles
SET created_at = DATE_SUB ( NOW ( ) , INTERVAL 2 HOUR )
WHERE host_uuid IN ( ? , ? , ? , ? ) ` ,
hostGlobal1 . UUID , hostGlobal2 . UUID , hostTeam . UUID , hostNoVars . UUID )
if err != nil {
return err
}
// Set host detail_updated_at to now (recent check-in)
_ , err = q . ExecContext ( ctx , ` UPDATE hosts SET detail_updated_at = NOW() WHERE id IN (?, ?, ?, ?) ` ,
hostGlobal1 . ID , hostGlobal2 . ID , hostTeam . ID , hostNoVars . ID )
return err
} )
// Helper to simulate osquery reporting back profile data
simulateOsqueryProfileReport := func ( nodeKey string , profileName string , locURI string , reportedData string ) {
// Build a SyncML response that osquery would send back after reading the profile from Windows
cmdRef := microsoft_mdm . HashLocURI ( profileName , locURI )
var msg fleet . SyncML
msg . Xmlns = syncml . SyncCmdNamespace
msg . SyncHdr = fleet . SyncHdr {
VerDTD : syncml . SyncMLSupportedVersion ,
VerProto : syncml . SyncMLVerProto ,
SessionID : "2" ,
MsgID : "2" ,
}
// Add status response (profile was successfully applied)
msg . AppendCommand ( fleet . MDMRaw , fleet . SyncMLCmd {
XMLName : xml . Name { Local : fleet . CmdStatus } ,
CmdID : fleet . CmdID { Value : uuid . NewString ( ) } ,
CmdRef : & cmdRef ,
Data : ptr . String ( "200" ) ,
} )
// Add results with the data that osquery read from Windows
msg . AppendCommand ( fleet . MDMRaw , fleet . SyncMLCmd {
XMLName : xml . Name { Local : fleet . CmdResults } ,
CmdID : fleet . CmdID { Value : uuid . NewString ( ) } ,
CmdRef : & cmdRef ,
Items : [ ] fleet . CmdItem {
{
Target : ptr . String ( locURI ) ,
Data : & fleet . RawXmlData {
Content : reportedData ,
} ,
} ,
} ,
} )
rawResponse , err := xml . Marshal ( msg )
require . NoError ( t , err )
// Submit the results via osquery distributed write endpoint
distributedReq := SubmitDistributedQueryResultsRequest {
NodeKey : nodeKey ,
Results : map [ string ] [ ] map [ string ] string {
"fleet_detail_query_mdm_config_profiles_windows" : {
{ "raw_mdm_command_output" : string ( rawResponse ) } ,
} ,
} ,
Statuses : map [ string ] fleet . OsqueryStatus {
"fleet_detail_query_mdm_config_profiles_windows" : 0 ,
} ,
}
distributedResp := submitDistributedQueryResultsResponse { }
s . DoJSON ( "POST" , "/api/osquery/distributed/write" , distributedReq , http . StatusOK , & distributedResp )
2025-08-10 10:24:38 +00:00
}
2025-08-11 12:47:55 +00:00
// First verify that normal profile (without variables) verifies correctly
simulateOsqueryProfileReport (
* hostNoVars . NodeKey ,
"ProfileNoVars" ,
"./Device/Vendor/MSFT/DMClient/Provider/ProviderID/Static/Value" ,
"Static Value: NoSubstitution" , // osquery reports exactly what was sent
)
// Normal profile should be verified successfully
checkHostProfileStatus ( hostNoVars . UUID , "ProfileNoVars" , fleet . MDMDeliveryVerified )
// Simulate osquery reporting back for team host
simulateOsqueryProfileReport (
* hostTeam . NodeKey ,
"TeamProfileWithVar" ,
"./Device/Vendor/MSFT/DMClient/Provider/ProviderID/TeamDevice/ID" ,
"Team Device: " + hostTeam . UUID , // osquery reports the substituted value
)
// Team host has TeamProfileWithVar which now correctly verifies with Fleet variables
// The fix has been implemented and the profile should be verified successfully
checkHostProfileStatus ( hostTeam . UUID , "TeamProfileWithVar" , fleet . MDMDeliveryVerified )
// Hit the host details API and check the status in the mdm.profiles section
// Verify team host
var hostRespTeam getHostResponse
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/v1/fleet/hosts/%d" , hostTeam . ID ) , getHostRequest { } , http . StatusOK , & hostRespTeam )
require . NotNil ( t , hostRespTeam . Host . MDM . Profiles )
require . Len ( t , * hostRespTeam . Host . MDM . Profiles , 1 )
require . Equal ( t , "TeamProfileWithVar" , ( * hostRespTeam . Host . MDM . Profiles ) [ 0 ] . Name )
2025-12-13 19:58:35 +00:00
require . EqualValues ( t , fleet . MDMDeliveryVerified , * ( * hostRespTeam . Host . MDM . Profiles ) [ 0 ] . Status ,
2025-08-11 12:47:55 +00:00
"Profile should be verified in host details API for team host" )
// Verify no-vars host
var hostRespNoVars getHostResponse
s . DoJSON ( "GET" , fmt . Sprintf ( "/api/v1/fleet/hosts/%d" , hostNoVars . ID ) , getHostRequest { } , http . StatusOK , & hostRespNoVars )
require . NotNil ( t , hostRespNoVars . Host . MDM . Profiles )
require . Len ( t , * hostRespNoVars . Host . MDM . Profiles , 1 )
require . Equal ( t , "ProfileNoVars" , ( * hostRespNoVars . Host . MDM . Profiles ) [ 0 ] . Name )
2025-12-13 19:58:35 +00:00
require . EqualValues ( t , fleet . MDMDeliveryVerified , * ( * hostRespNoVars . Host . MDM . Profiles ) [ 0 ] . Status ,
2025-08-11 12:47:55 +00:00
"Profile should be verified in host details API for no-vars host" )
2025-08-10 10:24:38 +00:00
}
2025-11-12 19:59:09 +00:00
2025-11-14 15:44:14 +00:00
//go:embed testdata/profiles/windows-device-scep.xml
var windowsDeviceSCEPProfileBytes [ ] byte
2025-11-12 19:59:09 +00:00
2025-11-14 15:44:14 +00:00
func ( s * integrationMDMTestSuite ) TestWindowsDeviceSCEPProfile ( ) {
testWindowsSCEPProfile ( s , windowsDeviceSCEPProfileBytes )
}
//go:embed testdata/profiles/windows-user-scep.xml
var windowsUserSCEPProfileBytes [ ] byte
func ( s * integrationMDMTestSuite ) TestWindowsUserSCEPProfile ( ) {
testWindowsSCEPProfile ( s , windowsUserSCEPProfileBytes )
}
func testWindowsSCEPProfile ( s * integrationMDMTestSuite , windowsScepProfile [ ] byte ) {
2025-11-12 19:59:09 +00:00
t := s . T ( )
ctx := context . Background ( )
scepServer := scep_server . StartTestSCEPServer ( t )
scepServerURL := scepServer . URL + "/scep"
// Create windows host and enroll in MDM
host , mdmDevice := createWindowsHostThenEnrollMDM ( s . ds , s . server . URL , t )
verifyCommands := func ( wantProfiles int , status string ) {
cmds , err := mdmDevice . StartManagementSession ( )
require . NoError ( t , err )
// profile installs + 2 protocol commands acks
require . Len ( t , cmds , wantProfiles + 2 )
msgID , err := mdmDevice . GetCurrentMsgID ( )
require . NoError ( t , err )
atomicCmds := 0
for _ , c := range cmds {
if c . Verb == "Atomic" {
atomicCmds ++
}
mdmDevice . AppendResponse ( fleet . SyncMLCmd {
XMLName : xml . Name { Local : fleet . CmdStatus } ,
MsgRef : & msgID ,
CmdRef : ptr . String ( c . Cmd . CmdID . Value ) ,
Cmd : ptr . String ( c . Verb ) ,
Data : ptr . String ( status ) ,
Items : nil ,
CmdID : fleet . CmdID { Value : uuid . NewString ( ) } ,
} )
}
require . Equal ( t , wantProfiles , atomicCmds )
cmds , err = mdmDevice . SendResponse ( )
require . NoError ( t , err )
// the ack of the message should be the only returned command
require . Len ( t , cmds , 1 )
}
// Upload SCEP profile with missing CA
resp := s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" ,
batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
2025-11-14 15:44:14 +00:00
{ Name : "WindowsSCEPProfile" , Contents : windowsScepProfile } ,
2025-11-12 19:59:09 +00:00
} } ,
http . StatusBadRequest )
errMsg := extractServerErrorText ( resp . Body )
require . Contains ( t , errMsg , "Fleet variable $FLEET_VAR_CUSTOM_SCEP_PROXY_URL_INTEGRATION does not exist." )
// Create Custom SCEP CA
ca := & fleet . CertificateAuthority {
Type : string ( fleet . CATypeCustomSCEPProxy ) ,
Name : ptr . String ( "INTEGRATION" ) ,
Challenge : ptr . String ( "integration-test" ) ,
URL : ptr . String ( scepServerURL ) ,
}
_ , err := s . ds . NewCertificateAuthority ( ctx , ca )
require . NoError ( t , err )
2025-12-12 17:14:59 +00:00
// Fail on missing OU
resp = s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" ,
batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : "WindowsSCEPProfile" , Contents : bytes . ReplaceAll ( windowsScepProfile , [ ] byte ( fleet . FleetVarSCEPRenewalID . WithPrefix ( ) ) , [ ] byte ( "BOGUS" ) ) } ,
} } ,
http . StatusBadRequest )
errMsg = extractServerErrorText ( resp . Body )
require . Contains ( t , errMsg , "SCEP profile for custom SCEP certificate authority requires: $FLEET_VAR_CUSTOM_SCEP_CHALLENGE_<CA_NAME>, $FLEET_VAR_CUSTOM_SCEP_PROXY_URL_<CA_NAME>, and $FLEET_VAR_SCEP_RENEWAL_ID variables" )
2025-11-12 19:59:09 +00:00
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" ,
batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
2025-11-14 15:44:14 +00:00
{ Name : "WindowsSCEPProfile" , Contents : windowsScepProfile } ,
2025-11-12 19:59:09 +00:00
} } ,
http . StatusNoContent )
// Verify host receives the profile
s . awaitTriggerProfileSchedule ( t )
// Check that profile status is Pending
profiles , err := s . ds . GetHostMDMWindowsProfiles ( ctx , host . UUID )
require . NoError ( t , err )
var foundProfile bool
for _ , p := range profiles {
if p . Name == "WindowsSCEPProfile" {
foundProfile = true
require . NotNil ( t , p . Status )
2025-12-13 19:58:35 +00:00
assert . EqualValues ( t , fleet . MDMDeliveryPending , * p . Status )
2025-11-12 19:59:09 +00:00
}
}
require . True ( t , foundProfile , "WindowsSCEPProfile not found for host" )
verifyCommands ( 1 , syncml . CmdStatusOK )
// Verify profile status is Verified due to successful response
profiles , err = s . ds . GetHostMDMWindowsProfiles ( ctx , host . UUID )
require . NoError ( t , err )
foundProfile = false
for _ , p := range profiles {
if p . Name == "WindowsSCEPProfile" {
foundProfile = true
require . NotNil ( t , p . Status )
2025-12-13 19:58:35 +00:00
assert . EqualValues ( t , fleet . MDMDeliveryVerified , * p . Status )
2025-11-12 19:59:09 +00:00
}
}
require . True ( t , foundProfile , "WindowsSCEPProfile not found for host" )
// Report Osquery results indicating SCEP profile was applied successfully
s . reportWindowsOSQueryProfiles ( ctx , t , host , map [ string ] [ ] profileData {
"WindowsSCEPProfile" : { { "200" , "L1" , "Bogus" } } , // Report back with SCEP LocURI, but data that does not relate SCEP to support the case that we don't verify the success.
} )
// Verify profile status is still Verified, and OSQuery does not change it's status.
profiles , err = s . ds . GetHostMDMWindowsProfiles ( ctx , host . UUID )
require . NoError ( t , err )
foundProfile = false
profileUUID := ""
for _ , p := range profiles {
if p . Name == "WindowsSCEPProfile" {
foundProfile = true
profileUUID = p . ProfileUUID
require . NotNil ( t , p . Status )
2025-12-13 19:58:35 +00:00
require . EqualValues ( t , fleet . MDMDeliveryVerified , * p . Status )
2025-11-12 19:59:09 +00:00
}
}
require . True ( t , foundProfile , "WindowsSCEPProfile not found for host" )
// Attempt simple SCEP call with GetCACaps operation to verify SCEP server is reachable
identifier := host . UUID + "," + profileUUID + "," + "INTEGRATION"
scepRes := s . DoRawWithHeaders ( "GET" , apple_mdm . SCEPProxyPath + identifier + "/pkiclient.exe" , nil , http . StatusOK , nil , "operation" , "GetCACaps" )
body , err := io . ReadAll ( scepRes . Body )
require . NoError ( t , err )
assert . Equal ( t , scepserver . DefaultCACaps , string ( body ) )
}
2025-11-21 14:13:36 +00:00
// This test verifies that there is no longer a race condition in apple profile resending
func ( s * integrationMDMTestSuite ) TestAppleProfileResendRaceCondition ( ) {
t := s . T ( )
ctx := context . Background ( )
// Create a host and enroll it in MDM
host , mdmDevice := createHostThenEnrollMDM ( s . ds , s . server . URL , t )
setupPusher ( s , t , mdmDevice )
scimUserID , err := s . ds . CreateScimUser ( ctx , & fleet . ScimUser { UserName : "user@example.com" } )
require . NoError ( t , err )
// Assign scim user to host
hostIdStr := fmt . Sprint ( host . ID )
s . Do ( "PUT" , "/api/latest/fleet/hosts/" + hostIdStr + "/device_mapping" , putHostDeviceMappingRequest {
Email : "user@example.com" ,
Source : "idp" ,
} , http . StatusOK )
// Create a profile that uses IDP variables
profileWithIDPVar := mobileconfigForTestWithContent ( "TestProfile" , "com.test.profile" , "com.test.profile.content" , "com.test.profile" , "Test IDP Variable Profile" )
// Replace the profile content to include an IDP variable
profileContent := string ( profileWithIDPVar )
profileContent = strings . Replace ( profileContent , "<key>ShowRecoveryKey</key>" , "<key>TestVariable</key>" , 1 )
profileContent = strings . Replace ( profileContent , "<false/>" , "<string>$FLEET_VAR_HOST_END_USER_IDP_USERNAME</string>" , 1 )
profileWithIDPVar = [ ] byte ( profileContent )
// Upload the profile using the new endpoint that supports variables
s . Do ( "POST" , "/api/latest/fleet/mdm/profiles/batch" , batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : "TestProfile" , Contents : profileWithIDPVar } ,
} } , http . StatusNoContent ) // Setup SCIM user data for the host
// No profiles until reconciler
hostProfiles , err := s . ds . GetHostMDMAppleProfiles ( ctx , host . UUID )
require . NoError ( t , err )
require . Empty ( t , hostProfiles , "Host should not have any profiles before sync" )
// Trigger initial profile sync - profile should be set to pending/installing
s . awaitTriggerProfileSchedule ( t )
// Check that install command was sent, but do not acknowledge it yet
cmd , err := mdmDevice . Idle ( )
require . NoError ( t , err )
seenProfile := false
profileCmdID := ""
for cmd != nil {
if cmd . Command . RequestType != "InstallProfile" {
cmd , err = mdmDevice . Acknowledge ( cmd . CommandUUID )
require . NoError ( t , err )
continue
}
var fullCmd micromdm . CommandPayload
err = plist . Unmarshal ( cmd . Raw , & fullCmd )
require . NoError ( t , err )
if strings . Contains ( string ( fullCmd . Command . InstallProfile . Payload ) , "TestProfile" ) {
seenProfile = true
profileCmdID = cmd . CommandUUID
break
}
// Acknowledge other commands
cmd , err = mdmDevice . Acknowledge ( cmd . CommandUUID )
require . NoError ( t , err )
}
require . True ( t , seenProfile , "Expected install command for TestProfile not found" )
// Verify profile is in pending status
hostProfiles , err = s . ds . GetHostMDMAppleProfiles ( ctx , host . UUID )
require . NoError ( t , err )
var testProfile * fleet . HostMDMAppleProfile
for _ , p := range hostProfiles {
if p . Identifier == "com.test.profile" {
testProfile = & p
break
}
}
require . NotNil ( t , testProfile )
2025-12-13 19:58:35 +00:00
require . EqualValues ( t , fleet . MDMDeliveryPending , * testProfile . Status )
2025-11-21 14:13:36 +00:00
// Now simulate the race condition:
// we trigger a resend before the acknowledgement comes back
// 1. Trigger an IDP variable change by updating SCIM user
err = s . ds . ReplaceScimUser ( ctx , & fleet . ScimUser { ID : scimUserID , UserName : "newuser@example.com" } )
require . NoError ( t , err )
// 2. At this point, the profile should be marked for resend (status = NULL)
hostProfiles , err = s . ds . GetHostMDMAppleProfiles ( ctx , host . UUID )
require . NoError ( t , err )
for _ , p := range hostProfiles {
if p . Identifier == "com.test.profile" {
fmt . Printf ( "%v\n" , p . Status )
testProfile = & p
break
}
}
require . NotNil ( t , testProfile )
2025-12-13 19:58:35 +00:00
require . EqualValues ( t , fleet . MDMDeliveryPending , * testProfile . Status ) // Should be NULL (pending for the user)
2025-11-21 14:13:36 +00:00
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
var status * fleet . MDMDeliveryStatus
err := sqlx . GetContext ( t . Context ( ) , q , & status , ` SELECT status FROM host_mdm_apple_profiles WHERE profile_identifier = ? ` , testProfile . Identifier )
require . Nil ( t , status )
return err
} )
// Acknowledge the original install command now, simulating the device response
cmd , err = mdmDevice . Acknowledge ( profileCmdID )
require . NoError ( t , err )
// Now check if we see any new TestProfile cmds (we need to ack them here, since we might have skipped some above.)
seenProfile = false
for cmd != nil {
if cmd . Command . RequestType != "InstallProfile" {
cmd , err = mdmDevice . Acknowledge ( cmd . CommandUUID )
require . NoError ( t , err )
continue
}
var fullCmd micromdm . CommandPayload
err = plist . Unmarshal ( cmd . Raw , & fullCmd )
require . NoError ( t , err )
if strings . Contains ( string ( fullCmd . Command . InstallProfile . Payload ) , "TestProfile" ) {
seenProfile = true
// Acknowledge the resend command
cmd , err = mdmDevice . Acknowledge ( cmd . CommandUUID )
require . NoError ( t , err )
} else {
// Acknowledge other commands
cmd , err = mdmDevice . Acknowledge ( cmd . CommandUUID )
require . NoError ( t , err )
}
}
require . Nil ( t , cmd , "No further commands should be pending after acknowledging install" )
require . False ( t , seenProfile , "No resend install command for TestProfile should be sent due to race condition" )
// Verify the profile is still in pending status (null in the DB)
// aka. no race condition.
hostProfiles , err = s . ds . GetHostMDMAppleProfiles ( ctx , host . UUID )
require . NoError ( t , err )
for _ , p := range hostProfiles {
if p . Identifier == "com.test.profile" {
testProfile = & p
break
}
}
require . NotNil ( t , testProfile )
2025-12-13 19:58:35 +00:00
require . EqualValues ( t , fleet . MDMDeliveryPending , * testProfile . Status )
2025-11-21 14:13:36 +00:00
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
var status * fleet . MDMDeliveryStatus
err := sqlx . GetContext ( t . Context ( ) , q , & status , ` SELECT status FROM host_mdm_apple_profiles WHERE profile_identifier = ? ` , testProfile . Identifier )
require . Nil ( t , status )
return err
} )
// run reconciler to resend any pending profiles
s . awaitTriggerProfileSchedule ( t )
cmd , err = mdmDevice . Idle ( )
require . NoError ( t , err )
// Now we should see the resend command
seenProfile = false
for cmd != nil {
if cmd . Command . RequestType != "InstallProfile" {
cmd , err = mdmDevice . Acknowledge ( cmd . CommandUUID )
require . NoError ( t , err )
continue
}
var fullCmd micromdm . CommandPayload
err = plist . Unmarshal ( cmd . Raw , & fullCmd )
require . NoError ( t , err )
if strings . Contains ( string ( fullCmd . Command . InstallProfile . Payload ) , "TestProfile" ) {
seenProfile = true
// Acknowledge the resend command
cmd , err = mdmDevice . Acknowledge ( cmd . CommandUUID )
require . NoError ( t , err )
} else {
// Acknowledge other commands
cmd , err = mdmDevice . Acknowledge ( cmd . CommandUUID )
require . NoError ( t , err )
}
}
require . True ( t , seenProfile , "Resend install command for TestProfile should be sent after reconciler runs" )
// And we should now also see the profile being marked as verifying
hostProfiles , err = s . ds . GetHostMDMAppleProfiles ( ctx , host . UUID )
require . NoError ( t , err )
for _ , p := range hostProfiles {
if p . Identifier == "com.test.profile" {
testProfile = & p
break
}
}
require . NotNil ( t , testProfile )
2025-12-13 19:58:35 +00:00
require . EqualValues ( t , fleet . MDMDeliveryVerifying , * testProfile . Status )
2025-11-21 14:13:36 +00:00
}
2025-12-10 23:04:17 +00:00
func ( s * integrationMDMTestSuite ) TestWindowsProfileRetry ( ) {
t := s . T ( )
ctx := t . Context ( )
// Create a host and enroll it in MDM
host , mdmDevice := createWindowsHostThenEnrollMDM ( s . ds , s . server . URL , t )
t . Run ( "Command gets retried with Replace after 418" , func ( t * testing . T ) {
profilePayload := syncml . ForTestWithData ( [ ] syncml . TestCommand {
{ Verb : "Add" , LocURI : "./Device/Vendor/MSFT/Policy/Config/System/AllowLocation" , Data : "1" } ,
} )
profileName := "RetryProfile"
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" ,
batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : profileName , Contents : profilePayload } ,
} } ,
http . StatusNoContent )
expectRetry := func ( profileName string , expectedRetries int ) {
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
var retryCount int
err := sqlx . GetContext ( t . Context ( ) , q , & retryCount ,
` SELECT retries FROM host_mdm_windows_profiles WHERE host_uuid = ? AND profile_name = ? ` ,
host . UUID , profileName )
require . NoError ( t , err )
require . Equal ( t , expectedRetries , retryCount , "Unexpected retry count for profile %s" , profileName )
return nil
} )
}
// Trigger profile schedule
s . awaitTriggerProfileSchedule ( t )
// Get initial host profile
profiles , err := s . ds . GetHostMDMWindowsProfiles ( ctx , host . UUID )
require . NoError ( t , err )
var initialProfile fleet . HostMDMWindowsProfile
for _ , p := range profiles {
if p . Name == profileName {
initialProfile = p
break
}
}
require . NotNil ( t , initialProfile )
require . Equal ( t , fleet . MDMDeliveryPending , * initialProfile . Status )
cmds , err := mdmDevice . StartManagementSession ( )
require . NoError ( t , err )
msgID , err := mdmDevice . GetCurrentMsgID ( )
require . NoError ( t , err )
for _ , cmd := range cmds {
if cmd . Verb == "Status" {
continue
}
syncCmd := fleet . SyncMLCmd {
XMLName : xml . Name { Local : fleet . CmdStatus } ,
MsgRef : & msgID ,
CmdRef : & cmd . Cmd . CmdID . Value ,
Cmd : ptr . String ( cmd . Verb ) ,
Data : ptr . String ( syncml . CmdStatusAtomicFailed ) ,
CmdID : fleet . CmdID { Value : uuid . NewString ( ) } ,
}
mdmDevice . AppendResponse ( syncCmd )
for _ , addCmd := range cmd . Cmd . AddCommands {
for range addCmd . Items {
itemCmd := fleet . SyncMLCmd {
XMLName : xml . Name { Local : fleet . CmdStatus } ,
MsgRef : & msgID ,
CmdRef : & addCmd . CmdID . Value ,
Cmd : ptr . String ( fleet . CmdStatus ) ,
// 418 triggers Replace resend logic
Data : ptr . String ( syncml . CmdStatusAlreadyExists ) ,
CmdID : fleet . CmdID { Value : uuid . NewString ( ) } ,
}
mdmDevice . AppendResponse ( itemCmd )
}
}
}
cmds , err = mdmDevice . SendResponse ( ) // we have atomic replace (resend after 418 attempt in this cmd list here)
require . NoError ( t , err )
require . Len ( t , cmds , 2 ) // stsatus + atomic replace
// After initial 418 resend: pending, empty detail, retries = 0.
profiles , err = s . ds . GetHostMDMWindowsProfiles ( ctx , host . UUID )
require . NoError ( t , err )
var updatedProfile fleet . HostMDMWindowsProfile
for _ , p := range profiles {
if p . Name == profileName {
updatedProfile = p
break
}
}
require . NotNil ( t , updatedProfile )
require . Equal ( t , fleet . MDMDeliveryPending , * updatedProfile . Status )
require . Empty ( t , updatedProfile . Detail )
expectRetry ( profileName , 0 )
// Second session: fail Atomic to trigger normal retry (status NULL, retries++ -> 1).
cmds , err = mdmDevice . StartManagementSession ( )
require . NoError ( t , err )
msgID , err = mdmDevice . GetCurrentMsgID ( )
require . NoError ( t , err )
for _ , cmd := range cmds {
if cmd . Verb == "Status" {
continue
}
syncCmd := fleet . SyncMLCmd {
XMLName : xml . Name { Local : fleet . CmdStatus } ,
MsgRef : & msgID ,
CmdRef : & cmd . Cmd . CmdID . Value ,
Cmd : ptr . String ( cmd . Verb ) ,
Data : ptr . String ( syncml . CmdStatusAtomicFailed ) ,
CmdID : fleet . CmdID { Value : uuid . NewString ( ) } ,
}
mdmDevice . AppendResponse ( syncCmd )
}
_ , err = mdmDevice . SendResponse ( )
require . NoError ( t , err )
// Verify raw DB status is NULL and retries = 1.
mysql . ExecAdhocSQL ( t , s . ds , func ( q sqlx . ExtContext ) error {
var status sql . NullString
var retries int
err := sqlx . GetContext ( t . Context ( ) , q , & status ,
` SELECT status FROM host_mdm_windows_profiles WHERE host_uuid = ? AND profile_name = ? ` ,
host . UUID , profileName )
require . NoError ( t , err )
require . False ( t , status . Valid , "status should be NULL" )
err = sqlx . GetContext ( t . Context ( ) , q , & retries ,
` SELECT retries FROM host_mdm_windows_profiles WHERE host_uuid = ? AND profile_name = ? ` ,
host . UUID , profileName )
require . NoError ( t , err )
require . Equal ( t , 1 , retries )
return nil
} )
// Third session: Add 418 again to requeue Replace; retries decremented back to 0.
s . awaitTriggerProfileSchedule ( t )
cmds , err = mdmDevice . StartManagementSession ( )
require . NoError ( t , err )
msgID , err = mdmDevice . GetCurrentMsgID ( )
require . NoError ( t , err )
for _ , cmd := range cmds {
if cmd . Verb == "Status" {
continue
}
syncCmd := fleet . SyncMLCmd {
XMLName : xml . Name { Local : fleet . CmdStatus } ,
MsgRef : & msgID ,
CmdRef : & cmd . Cmd . CmdID . Value ,
Cmd : ptr . String ( cmd . Verb ) ,
Data : ptr . String ( syncml . CmdStatusAtomicFailed ) ,
CmdID : fleet . CmdID { Value : uuid . NewString ( ) } ,
}
mdmDevice . AppendResponse ( syncCmd )
for _ , addCmd := range cmd . Cmd . AddCommands {
for range addCmd . Items {
mdmDevice . AppendResponse ( fleet . SyncMLCmd {
XMLName : xml . Name { Local : fleet . CmdStatus } ,
MsgRef : & msgID ,
CmdRef : & addCmd . CmdID . Value ,
Cmd : ptr . String ( fleet . CmdStatus ) ,
Data : ptr . String ( syncml . CmdStatusAlreadyExists ) , // 418
CmdID : fleet . CmdID { Value : uuid . NewString ( ) } ,
} )
}
}
}
newCmds , err := mdmDevice . SendResponse ( )
require . NoError ( t , err )
require . Len ( t , newCmds , 2 ) // status + atomic replace
// Pending and retries back to 0.
profiles , err = s . ds . GetHostMDMWindowsProfiles ( ctx , host . UUID )
require . NoError ( t , err )
for _ , p := range profiles {
if p . Name == profileName {
updatedProfile = p
break
}
}
require . NotNil ( t , updatedProfile )
require . Equal ( t , fleet . MDMDeliveryPending , * updatedProfile . Status )
require . Empty ( t , updatedProfile . Detail )
expectRetry ( profileName , 1 )
// Fourth session: Replace succeeds (Atomic OK + item 200) → verifying.
s . awaitTriggerProfileSchedule ( t )
cmds , err = mdmDevice . StartManagementSession ( )
require . NoError ( t , err )
msgID , err = mdmDevice . GetCurrentMsgID ( )
require . NoError ( t , err )
for _ , cmd := range cmds {
syncCmd := fleet . SyncMLCmd {
XMLName : xml . Name { Local : fleet . CmdStatus } ,
MsgRef : & msgID ,
CmdRef : & cmd . Cmd . CmdID . Value ,
Cmd : ptr . String ( cmd . Verb ) ,
Data : ptr . String ( syncml . CmdStatusOK ) ,
CmdID : fleet . CmdID { Value : uuid . NewString ( ) } ,
}
mdmDevice . AppendResponse ( syncCmd )
for _ , repCmd := range cmd . Cmd . ReplaceCommands {
for _ , item := range repCmd . Items {
itemCmdRef := microsoft_mdm . HashLocURI ( profileName , * item . Target )
mdmDevice . AppendResponse ( fleet . SyncMLCmd {
XMLName : xml . Name { Local : fleet . CmdStatus } ,
MsgRef : & msgID ,
CmdRef : & itemCmdRef ,
Cmd : ptr . String ( fleet . CmdStatus ) ,
Data : ptr . String ( syncml . CmdStatusOK ) , // 200
CmdID : fleet . CmdID { Value : uuid . NewString ( ) } ,
} )
}
}
}
_ , err = mdmDevice . SendResponse ( )
require . NoError ( t , err )
profiles , err = s . ds . GetHostMDMWindowsProfiles ( ctx , host . UUID )
require . NoError ( t , err )
for _ , p := range profiles {
if p . Name == profileName {
updatedProfile = p
break
}
}
require . NotNil ( t , updatedProfile )
require . Equal ( t , fleet . MDMDeliveryVerifying , * updatedProfile . Status )
require . Empty ( t , updatedProfile . Detail )
expectRetry ( profileName , 1 )
} )
t . Run ( "No resend on non-retryable error" , func ( t * testing . T ) {
profilePayload2 := syncml . ForTestWithData ( [ ] syncml . TestCommand {
{ Verb : "Add" , LocURI : "./Device/Vendor/MSFT/Policy/Config/System/AllowCamera" , Data : "1" } ,
} )
profileName2 := "NonRetryProfile"
s . Do ( "POST" , "/api/v1/fleet/mdm/profiles/batch" ,
batchSetMDMProfilesRequest { Profiles : [ ] fleet . MDMProfileBatchPayload {
{ Name : profileName2 , Contents : profilePayload2 } ,
} } ,
http . StatusNoContent )
// Trigger profile schedule
s . awaitTriggerProfileSchedule ( t )
// Get initial host profile
profiles , err := s . ds . GetHostMDMWindowsProfiles ( ctx , host . UUID )
require . NoError ( t , err )
var initialProfile2 fleet . HostMDMWindowsProfile
for _ , p := range profiles {
if p . Name == profileName2 {
initialProfile2 = p
break
}
}
require . NotNil ( t , initialProfile2 )
require . Equal ( t , fleet . MDMDeliveryPending , * initialProfile2 . Status )
cmds , err := mdmDevice . StartManagementSession ( )
require . NoError ( t , err )
msgID , err := mdmDevice . GetCurrentMsgID ( )
require . NoError ( t , err )
for _ , cmd := range cmds {
if cmd . Verb == "Status" {
continue
}
syncCmd := fleet . SyncMLCmd {
XMLName : xml . Name { Local : fleet . CmdStatus } ,
MsgRef : & msgID ,
CmdRef : & cmd . Cmd . CmdID . Value ,
Cmd : ptr . String ( cmd . Verb ) ,
Data : ptr . String ( syncml . CmdStatusAtomicFailed ) , // Generic failure, should not retry
CmdID : fleet . CmdID { Value : uuid . NewString ( ) } ,
}
mdmDevice . AppendResponse ( syncCmd )
for _ , addCmd := range cmd . Cmd . AddCommands {
for range addCmd . Items {
itemCmd := fleet . SyncMLCmd {
XMLName : xml . Name { Local : fleet . CmdStatus } ,
MsgRef : & msgID ,
CmdRef : & addCmd . CmdID . Value ,
Cmd : ptr . String ( fleet . CmdStatus ) ,
// 500 generic failure
Data : ptr . String ( syncml . CmdStatusBadRequest ) ,
CmdID : fleet . CmdID { Value : uuid . NewString ( ) } ,
}
mdmDevice . AppendResponse ( itemCmd )
}
}
}
cmds , err = mdmDevice . SendResponse ( )
require . NoError ( t , err )
require . Len ( t , cmds , 1 ) // only ack returned
} )
}