BRP: add batch-resend profile to hosts endpoint based on status (#28871)
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user