BRP: add batch-resend profile to hosts endpoint based on status (#28871)

This commit is contained in:
Martin Angers
2025-05-07 16:48:18 -04:00
committed by GitHub
parent e92feb17b7
commit 48de857dca
13 changed files with 549 additions and 2 deletions
@@ -0,0 +1 @@
* Added the endpoint `POST /api/v1/fleet/configuration_profiles/resend/batch` to resend a profile to all hosts that satisfy the filter.
+18 -1
View File
@@ -1185,7 +1185,7 @@ This activity contains the following fields:
## resent_configuration_profile
Generated when a user resends an MDM configuration profile to a host.
Generated when a user resends a configuration profile to a host.
This activity contains the following fields:
- "host_id": The ID of the host.
@@ -1202,6 +1202,23 @@ This activity contains the following fields:
}
```
## resent_configuration_profile_batch
Generated when a user resends a configuration profile to a batch of hosts.
This activity contains the following fields:
- "profile_name": The name of the configuration profile.
- "host_count": Number of hosts in the batch.
#### Example
```json
{
"profile_name": "Passcode requirements",
"host_count": 3
}
```
## installed_software
Generated when a Fleet-maintained app or custom package is installed on a host.
+21
View File
@@ -1615,3 +1615,24 @@ func batchSetProfileVariableAssociationsDB(
}
return nil
}
func (ds *Datastore) BatchResendMDMProfileToHosts(ctx context.Context, profileUUID string, filters fleet.BatchResendMDMProfileFilters) (int64, error) {
table, column, err := getTableAndColumnNameForHostMDMProfileUUID(profileUUID)
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "getting table and column")
}
// update the status to NULL to trigger resending on the next cron run
updateStmt := fmt.Sprintf(`UPDATE %s SET status = NULL WHERE %s = ? AND status = ?`, table, column)
var count int64
err = ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
res, err := tx.ExecContext(ctx, updateStmt, profileUUID, filters.ProfileStatus)
if err != nil {
return ctxerr.Wrap(ctx, err, "resending MDM profile on hosts")
}
count, _ = res.RowsAffected()
return nil
})
return count, err
}
+168
View File
@@ -49,6 +49,7 @@ func TestMDMShared(t *testing.T) {
{"TestAreHostsConnectedToFleetMDM", testAreHostsConnectedToFleetMDM},
{"TestBulkSetPendingMDMHostProfilesExcludeAny", testBulkSetPendingMDMHostProfilesExcludeAny},
{"TestBulkSetPendingMDMHostProfilesLotsOfHosts", testBulkSetPendingMDMWindowsHostProfilesLotsOfHosts},
{"TestBatchResendProfileToHosts", testBatchResendProfileToHosts},
}
for _, c := range cases {
@@ -7707,3 +7708,170 @@ func testBulkSetPendingMDMWindowsHostProfilesLotsOfHosts(t *testing.T, ds *Datas
_, err := ds.bulkSetPendingMDMWindowsHostProfilesDB(ctx, ds.writer(ctx), hostUUIDs, nil)
require.NoError(t, err)
}
func testBatchResendProfileToHosts(t *testing.T, ds *Datastore) {
ctx := t.Context()
// create some hosts and some profiles
host1 := test.NewHost(t, ds, "host1", "1", "h1key", "host1uuid", time.Now())
host2 := test.NewHost(t, ds, "host2", "2", "h2key", "host2uuid", time.Now())
host3 := test.NewHost(t, ds, "host3", "3", "h3key", "host3uuid", time.Now())
host4 := test.NewHost(t, ds, "host4", "4", "h4key", "host4uuid", time.Now())
// create a team and make host4 part of that team
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
require.NoError(t, err)
err = ds.AddHostsToTeam(ctx, &team.ID, []uint{host4.ID})
require.NoError(t, err)
// create some profiles , a and b for no team, c for team
profA, err := ds.NewMDMAppleConfigProfile(ctx, *generateCP("a", "a", 0), nil)
require.NoError(t, err)
profB, err := ds.NewMDMAppleConfigProfile(ctx, *generateCP("b", "b", 0), nil)
require.NoError(t, err)
profC, err := ds.NewMDMAppleConfigProfile(ctx, *generateCP("c", "c", team.ID), nil)
require.NoError(t, err)
t.Logf("profA=%s, profB=%s, profC=%s", profA.ProfileUUID, profB.ProfileUUID, profC.ProfileUUID)
assertHostProfileStatus(t, ds, host1.UUID)
assertHostProfileStatus(t, ds, host2.UUID)
assertHostProfileStatus(t, ds, host3.UUID)
assertHostProfileStatus(t, ds, host4.UUID)
// make profile A installed for all no team
forceSetHostProfileStatus(t, ds, host1.UUID, profA, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
forceSetHostProfileStatus(t, ds, host2.UUID, profA, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
forceSetHostProfileStatus(t, ds, host3.UUID, profA, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
// batch-resend profile A, does not impact any host
n, err := ds.BatchResendMDMProfileToHosts(ctx, profA.ProfileUUID, fleet.BatchResendMDMProfileFilters{ProfileStatus: fleet.MDMDeliveryFailed})
require.NoError(t, err)
require.EqualValues(t, 0, n)
assertHostProfileStatus(t, ds, host1.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerifying})
assertHostProfileStatus(t, ds, host2.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerifying})
assertHostProfileStatus(t, ds, host3.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerifying})
assertHostProfileStatus(t, ds, host4.UUID)
// batch-resend profile B, does not impact any host as it's not delievered to any yet
n, err = ds.BatchResendMDMProfileToHosts(ctx, profB.ProfileUUID, fleet.BatchResendMDMProfileFilters{ProfileStatus: fleet.MDMDeliveryFailed})
require.NoError(t, err)
require.EqualValues(t, 0, n)
assertHostProfileStatus(t, ds, host1.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerifying})
assertHostProfileStatus(t, ds, host2.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerifying})
assertHostProfileStatus(t, ds, host3.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerifying})
assertHostProfileStatus(t, ds, host4.UUID)
// make profile A failed on a couple hosts, verified on the other
forceSetHostProfileStatus(t, ds, host1.UUID, profA, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryFailed)
forceSetHostProfileStatus(t, ds, host2.UUID, profA, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryFailed)
forceSetHostProfileStatus(t, ds, host3.UUID, profA, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerified)
// batch-resend profile A, impacts host 1 and 2
n, err = ds.BatchResendMDMProfileToHosts(ctx, profA.ProfileUUID, fleet.BatchResendMDMProfileFilters{ProfileStatus: fleet.MDMDeliveryFailed})
require.NoError(t, err)
require.EqualValues(t, 2, n)
assertHostProfileStatus(t, ds, host1.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryPending})
assertHostProfileStatus(t, ds, host2.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryPending})
assertHostProfileStatus(t, ds, host3.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerified})
assertHostProfileStatus(t, ds, host4.UUID)
// batch-resend profile A again, no change as it is already pending on the impacted hosts
n, err = ds.BatchResendMDMProfileToHosts(ctx, profA.ProfileUUID, fleet.BatchResendMDMProfileFilters{ProfileStatus: fleet.MDMDeliveryFailed})
require.NoError(t, err)
require.EqualValues(t, 0, n)
assertHostProfileStatus(t, ds, host1.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryPending})
assertHostProfileStatus(t, ds, host2.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryPending})
assertHostProfileStatus(t, ds, host3.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerified})
assertHostProfileStatus(t, ds, host4.UUID)
// make profile B failed on all hosts
forceSetHostProfileStatus(t, ds, host1.UUID, profB, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryFailed)
forceSetHostProfileStatus(t, ds, host2.UUID, profB, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryFailed)
forceSetHostProfileStatus(t, ds, host3.UUID, profB, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryFailed)
assertHostProfileStatus(t, ds, host1.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryPending},
hostProfileStatus{profB.ProfileUUID, fleet.MDMDeliveryFailed})
assertHostProfileStatus(t, ds, host2.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryPending},
hostProfileStatus{profB.ProfileUUID, fleet.MDMDeliveryFailed})
assertHostProfileStatus(t, ds, host3.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerified},
hostProfileStatus{profB.ProfileUUID, fleet.MDMDeliveryFailed})
assertHostProfileStatus(t, ds, host4.UUID)
// batch-resend profile B, all hosts affected
n, err = ds.BatchResendMDMProfileToHosts(ctx, profB.ProfileUUID, fleet.BatchResendMDMProfileFilters{ProfileStatus: fleet.MDMDeliveryFailed})
require.NoError(t, err)
require.EqualValues(t, 3, n)
assertHostProfileStatus(t, ds, host1.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryPending},
hostProfileStatus{profB.ProfileUUID, fleet.MDMDeliveryPending})
assertHostProfileStatus(t, ds, host2.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryPending},
hostProfileStatus{profB.ProfileUUID, fleet.MDMDeliveryPending})
assertHostProfileStatus(t, ds, host3.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerified},
hostProfileStatus{profB.ProfileUUID, fleet.MDMDeliveryPending})
assertHostProfileStatus(t, ds, host4.UUID)
// make profile C failed on host 4, other profiles Verified
forceSetHostProfileStatus(t, ds, host4.UUID, profC, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryFailed)
forceSetHostProfileStatus(t, ds, host1.UUID, profA, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerified)
forceSetHostProfileStatus(t, ds, host2.UUID, profA, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerified)
forceSetHostProfileStatus(t, ds, host1.UUID, profB, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerified)
forceSetHostProfileStatus(t, ds, host2.UUID, profB, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerified)
forceSetHostProfileStatus(t, ds, host3.UUID, profB, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerified)
// batch-resend profile C, host 4 affected
n, err = ds.BatchResendMDMProfileToHosts(ctx, profC.ProfileUUID, fleet.BatchResendMDMProfileFilters{ProfileStatus: fleet.MDMDeliveryFailed})
require.NoError(t, err)
require.EqualValues(t, 1, n)
assertHostProfileStatus(t, ds, host1.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerified},
hostProfileStatus{profB.ProfileUUID, fleet.MDMDeliveryVerified})
assertHostProfileStatus(t, ds, host2.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerified},
hostProfileStatus{profB.ProfileUUID, fleet.MDMDeliveryVerified})
assertHostProfileStatus(t, ds, host3.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerified},
hostProfileStatus{profB.ProfileUUID, fleet.MDMDeliveryVerified})
assertHostProfileStatus(t, ds, host4.UUID,
hostProfileStatus{profC.ProfileUUID, fleet.MDMDeliveryPending})
// batch-resend profile C again, no change
n, err = ds.BatchResendMDMProfileToHosts(ctx, profC.ProfileUUID, fleet.BatchResendMDMProfileFilters{ProfileStatus: fleet.MDMDeliveryFailed})
require.NoError(t, err)
require.EqualValues(t, 0, n)
assertHostProfileStatus(t, ds, host1.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerified},
hostProfileStatus{profB.ProfileUUID, fleet.MDMDeliveryVerified})
assertHostProfileStatus(t, ds, host2.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerified},
hostProfileStatus{profB.ProfileUUID, fleet.MDMDeliveryVerified})
assertHostProfileStatus(t, ds, host3.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerified},
hostProfileStatus{profB.ProfileUUID, fleet.MDMDeliveryVerified})
assertHostProfileStatus(t, ds, host4.UUID,
hostProfileStatus{profC.ProfileUUID, fleet.MDMDeliveryPending})
}
+21 -1
View File
@@ -173,6 +173,7 @@ var ActivityDetailsList = []ActivityDetails{
ActivityTypeEditedDeclarationProfile{},
ActivityTypeResentConfigurationProfile{},
ActivityTypeResentConfigurationProfileBatch{},
ActivityTypeInstalledSoftware{},
ActivityTypeUninstalledSoftware{},
@@ -1694,7 +1695,7 @@ func (a ActivityTypeResentConfigurationProfile) ActivityName() string {
}
func (a ActivityTypeResentConfigurationProfile) Documentation() (activity string, details string, detailsExample string) {
return `Generated when a user resends an MDM configuration profile to a host.`,
return `Generated when a user resends a configuration profile to a host.`,
`This activity contains the following fields:
- "host_id": The ID of the host.
- "host_display_name": The display name of the host.
@@ -1705,6 +1706,25 @@ func (a ActivityTypeResentConfigurationProfile) Documentation() (activity string
}`
}
type ActivityTypeResentConfigurationProfileBatch struct {
ProfileName string `json:"profile_name"`
HostCount int64 `json:"host_count"`
}
func (a ActivityTypeResentConfigurationProfileBatch) ActivityName() string {
return "resent_configuration_profile_batch"
}
func (a ActivityTypeResentConfigurationProfileBatch) Documentation() (activity string, details string, detailsExample string) {
return `Generated when a user resends a configuration profile to a batch of hosts.`,
`This activity contains the following fields:
- "profile_name": The name of the configuration profile.
- "host_count": Number of hosts in the batch.`, `{
"profile_name": "Passcode requirements",
"host_count": 3
}`
}
type ActivityTypeInstalledSoftware struct {
HostID uint `json:"host_id"`
HostDisplayName string `json:"host_display_name"`
+5
View File
@@ -1586,6 +1586,11 @@ type Datastore interface {
// to be resent upon the next cron run.
ResendHostMDMProfile(ctx context.Context, hostUUID string, profileUUID string) error
// BatchResendMDMProfileToHosts updates the profile status to NULL for the
// matching hosts that satisfy the filter, thereby triggering the profile to
// be resent upon the next cron run.
BatchResendMDMProfileToHosts(ctx context.Context, profileUUID string, filters BatchResendMDMProfileFilters) (int64, error)
// GetHostMDMProfileInstallStatus returns the status of the profile for the host.
GetHostMDMProfileInstallStatus(ctx context.Context, hostUUID string, profileUUID string) (MDMDeliveryStatus, error)
+6
View File
@@ -988,3 +988,9 @@ type MDMProfileIdentifierFleetVariables struct {
// findFleetVariables).
FleetVariables []string
}
// BatchResendMDMProfileFilters represents the filters to apply to hosts for
// batch-redelivery of an MDM profile.
type BatchResendMDMProfileFilters struct {
ProfileStatus MDMDeliveryStatus
}
+4
View File
@@ -1107,6 +1107,10 @@ type Service interface {
// ResendHostMDMProfile resends the MDM profile to the host.
ResendHostMDMProfile(ctx context.Context, hostID uint, profileUUID string) error
// BatchResendMDMProfileToHosts resends an MDM profile to the hosts that
// satisfy the specified filters.
BatchResendMDMProfileToHosts(ctx context.Context, profileUUID string, filters BatchResendMDMProfileFilters) error
///////////////////////////////////////////////////////////////////////////////
// Host Script Execution
+12
View File
@@ -1046,6 +1046,8 @@ type ListMDMConfigProfilesFunc func(ctx context.Context, teamID *uint, opt fleet
type ResendHostMDMProfileFunc func(ctx context.Context, hostUUID string, profileUUID string) error
type BatchResendMDMProfileToHostsFunc func(ctx context.Context, profileUUID string, filters fleet.BatchResendMDMProfileFilters) (int64, error)
type GetHostMDMProfileInstallStatusFunc func(ctx context.Context, hostUUID string, profileUUID string) (fleet.MDMDeliveryStatus, error)
type GetLinuxDiskEncryptionSummaryFunc func(ctx context.Context, teamID *uint) (fleet.MDMLinuxDiskEncryptionSummary, error)
@@ -2875,6 +2877,9 @@ type DataStore struct {
ResendHostMDMProfileFunc ResendHostMDMProfileFunc
ResendHostMDMProfileFuncInvoked bool
BatchResendMDMProfileToHostsFunc BatchResendMDMProfileToHostsFunc
BatchResendMDMProfileToHostsFuncInvoked bool
GetHostMDMProfileInstallStatusFunc GetHostMDMProfileInstallStatusFunc
GetHostMDMProfileInstallStatusFuncInvoked bool
@@ -6900,6 +6905,13 @@ func (s *DataStore) ResendHostMDMProfile(ctx context.Context, hostUUID string, p
return s.ResendHostMDMProfileFunc(ctx, hostUUID, profileUUID)
}
func (s *DataStore) BatchResendMDMProfileToHosts(ctx context.Context, profileUUID string, filters fleet.BatchResendMDMProfileFilters) (int64, error) {
s.mu.Lock()
s.BatchResendMDMProfileToHostsFuncInvoked = true
s.mu.Unlock()
return s.BatchResendMDMProfileToHostsFunc(ctx, profileUUID, filters)
}
func (s *DataStore) GetHostMDMProfileInstallStatus(ctx context.Context, hostUUID string, profileUUID string) (fleet.MDMDeliveryStatus, error) {
s.mu.Lock()
s.GetHostMDMProfileInstallStatusFuncInvoked = true
+1
View File
@@ -707,6 +707,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
// POST /hosts/{host_id:[0-9]+}/configuration_profiles/{profile_uuid}/resend endpoint.
mdmAnyMW.POST("/api/_version_/fleet/hosts/{host_id:[0-9]+}/configuration_profiles/resend/{profile_uuid}", resendHostMDMProfileEndpoint, resendHostMDMProfileRequest{})
mdmAnyMW.POST("/api/_version_/fleet/hosts/{host_id:[0-9]+}/configuration_profiles/{profile_uuid}/resend", resendHostMDMProfileEndpoint, resendHostMDMProfileRequest{})
mdmAnyMW.POST("/api/_version_/fleet/configuration_profiles/resend/batch", batchResendMDMProfileToHostsEndpoint, batchResendMDMProfileToHostsRequest{})
// Deprecated: PATCH /mdm/apple/settings is deprecated, replaced by POST /disk_encryption.
// It was only used to set disk encryption.
@@ -5909,3 +5909,191 @@ func (s *integrationMDMTestSuite) TestAppleProfileDeletion() {
require.NoError(t, err)
assert.Len(t, profiles, 3)
}
func (s *integrationMDMTestSuite) TestBatchResendMDMProfiles() {
t := s.T()
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)
forceSetAppleHostProfileStatus := func(hostUUID string, profile *fleet.MDMConfigProfilePayload, profileContent []byte, status fleet.MDMDeliveryStatus) {
ctx := t.Context()
mysql.ExecAdhocSQL(t, s.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, status, "install", uuid.NewString(), profile.Name, profileContent, profile.ProfileUUID)
return err
})
}
forceSetWindowsHostProfileStatus := func(hostUUID string, profile *fleet.MDMConfigProfilePayload, profileContent []byte, status fleet.MDMDeliveryStatus) {
ctx := t.Context()
mysql.ExecAdhocSQL(t, s.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, status, "install", uuid.NewString(), profile.Name, profileContent, profile.ProfileUUID)
return err
})
}
// 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 {
profNameToPayload[prof.Name] = prof
}
// 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)
forceSetAppleHostProfileStatus(host1.UUID, profNameToPayload["N1"], profN1, fleet.MDMDeliveryPending)
forceSetAppleHostProfileStatus(host1.UUID, profNameToPayload["N2"], profN2, fleet.MDMDeliveryPending)
forceSetAppleHostProfileStatus(host2.UUID, profNameToPayload["N1"], profN1, fleet.MDMDeliveryPending)
forceSetAppleHostProfileStatus(host2.UUID, profNameToPayload["N2"], profN2, fleet.MDMDeliveryPending)
forceSetWindowsHostProfileStatus(host3.UUID, profNameToPayload["N3"], profN3, fleet.MDMDeliveryPending)
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},
},
})
// acknowledge the Apple profiles, failing I2 on both hosts, and fail the Windows one
forceSetAppleHostProfileStatus(host1.UUID, profNameToPayload["N1"], profN1, fleet.MDMDeliveryVerifying)
forceSetAppleHostProfileStatus(host1.UUID, profNameToPayload["N2"], profN2, fleet.MDMDeliveryFailed)
forceSetAppleHostProfileStatus(host2.UUID, profNameToPayload["N1"], profN1, fleet.MDMDeliveryVerifying)
forceSetAppleHostProfileStatus(host2.UUID, profNameToPayload["N2"], profN2, fleet.MDMDeliveryFailed)
forceSetWindowsHostProfileStatus(host3.UUID, profNameToPayload["N3"], profN3, fleet.MDMDeliveryFailed)
// 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
forceSetAppleHostProfileStatus(host1.UUID, profNameToPayload["N2"], profN2, fleet.MDMDeliveryVerifying)
forceSetAppleHostProfileStatus(host2.UUID, profNameToPayload["N2"], profN2, fleet.MDMDeliveryVerifying)
// 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,
)
}
+97
View File
@@ -2805,3 +2805,100 @@ func (svc *Service) DeleteMDMAppleAPNSCert(ctx context.Context) error {
return svc.ds.SaveAppConfig(ctx, appCfg)
}
////////////////////////////////////////////////////////////////////////////////
// POST /configuration_profiles/resend/batch
////////////////////////////////////////////////////////////////////////////////
type batchResendMDMProfileToHostsRequest struct {
ProfileUUID string `json:"profile_uuid"`
Filters struct {
ProfileStatus string `json:"profile_status"`
} `json:"filters"`
}
type batchResendMDMProfileToHostsResponse struct {
Err error `json:"error,omitempty"`
}
func (r batchResendMDMProfileToHostsResponse) Error() error { return r.Err }
func (r batchResendMDMProfileToHostsResponse) Status() int { return http.StatusAccepted }
func batchResendMDMProfileToHostsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
req := request.(*batchResendMDMProfileToHostsRequest)
if err := svc.BatchResendMDMProfileToHosts(ctx, req.ProfileUUID, fleet.BatchResendMDMProfileFilters{
ProfileStatus: fleet.MDMDeliveryStatus(req.Filters.ProfileStatus),
}); err != nil {
return batchResendMDMProfileToHostsResponse{Err: err}, nil
}
return batchResendMDMProfileToHostsResponse{}, nil
}
func (svc *Service) BatchResendMDMProfileToHosts(ctx context.Context, profileUUID string, filters fleet.BatchResendMDMProfileFilters) error {
// do a basic authz check that before we can make a more specific check based
// on the team of the profile (just to ensure the user has _some_
// authorization before loading the profile).
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionSelectiveList); err != nil {
return ctxerr.Wrap(ctx, err)
}
switch filters.ProfileStatus {
case fleet.MDMDeliveryFailed:
// ok, only supported filter for now
default:
return &fleet.BadRequestError{
Message: "Invalid profile_status filter value, only 'failed' is currently supported.",
}
}
// get the profile to get the team it belongs to
var (
teamID *uint
profileName string
)
switch {
case strings.HasPrefix(profileUUID, fleet.MDMAppleProfileUUIDPrefix):
prof, err := svc.ds.GetMDMAppleConfigProfile(ctx, profileUUID)
if err != nil {
return err
}
teamID = prof.TeamID
profileName = prof.Name
case strings.HasPrefix(profileUUID, fleet.MDMWindowsProfileUUIDPrefix):
prof, err := svc.ds.GetMDMWindowsConfigProfile(ctx, profileUUID)
if err != nil {
return err
}
teamID = prof.TeamID
profileName = prof.Name
case strings.HasPrefix(profileUUID, fleet.MDMAppleDeclarationUUIDPrefix):
return &fleet.BadRequestError{
Message: "Can't resend declaration (DDM) profiles. Unlike configuration profiles (.mobileconfig), the host automatically checks in to get the latest DDM profiles.",
}
default:
return fleet.NewInvalidArgumentError("profile_uuid", "unknown profile").WithStatus(http.StatusNotFound)
}
// now we can do a write authz check based on team id of the host before proceeding
if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: teamID}, fleet.ActionWrite); err != nil {
return ctxerr.Wrap(ctx, err)
}
count, err := svc.ds.BatchResendMDMProfileToHosts(ctx, profileUUID, filters)
if err != nil {
return ctxerr.Wrap(ctx, err)
}
if count > 0 {
if err := svc.NewActivity(
ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeResentConfigurationProfileBatch{
ProfileName: profileName,
HostCount: count,
}); err != nil {
return ctxerr.Wrap(ctx, err, "logging activity for batch-resend of profile")
}
}
return nil
}
+7
View File
@@ -1998,6 +1998,9 @@ func TestMDMResendConfigProfileAuthz(t *testing.T) {
ds.NewActivityFunc = func(context.Context, *fleet.User, fleet.ActivityDetails, []byte, time.Time) error {
return nil
}
ds.BatchResendMDMProfileToHostsFunc = func(ctx context.Context, profUUID string, filters fleet.BatchResendMDMProfileFilters) (int64, error) {
return 0, nil
}
checkShouldFail := func(t *testing.T, err error, shouldFail bool) {
if !shouldFail {
@@ -2016,10 +2019,14 @@ func TestMDMResendConfigProfileAuthz(t *testing.T) {
// test authz resend config profile (no team)
err := svc.ResendHostMDMProfile(ctx, 1337, "a-no-team-profile")
checkShouldFail(t, err, tt.shouldFailGlobalWrite)
err = svc.BatchResendMDMProfileToHosts(ctx, "a-no-team-profile", fleet.BatchResendMDMProfileFilters{ProfileStatus: fleet.MDMDeliveryFailed})
checkShouldFail(t, err, tt.shouldFailGlobalWrite)
// test authz resend config profile (team 1)
err = svc.ResendHostMDMProfile(ctx, 1, "a-team-1-profile")
checkShouldFail(t, err, tt.shouldFailTeamWrite)
err = svc.BatchResendMDMProfileToHosts(ctx, "a-team-1-profile", fleet.BatchResendMDMProfileFilters{ProfileStatus: fleet.MDMDeliveryFailed})
checkShouldFail(t, err, tt.shouldFailTeamWrite)
})
}
}