implement status reports for DDM commands (#17831)

for #17408
This commit is contained in:
Roberto Dip 2024-03-26 10:40:35 -03:00 committed by GitHub
parent 44727ace3b
commit f0ad942a57
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 429 additions and 11 deletions

View File

@ -9,6 +9,7 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
@ -21,6 +22,7 @@ import (
"time"
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
"github.com/fleetdm/fleet/v4/server/fleet"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
"github.com/fleetdm/fleet/v4/server/mdm/scep/cryptoutil/x509util"
@ -390,7 +392,7 @@ func (c *TestAppleMDMClient) TokenUpdate() error {
// The endpoint argument is used as the value for the `Endpoint` key in the request payload.
//
// For more details check https://developer.apple.com/documentation/devicemanagement/declarativemanagementrequest
func (c *TestAppleMDMClient) DeclarativeManagement(endpoint string) (*http.Response, error) {
func (c *TestAppleMDMClient) DeclarativeManagement(endpoint string, data ...fleet.MDMAppleDDMStatusReport) (*http.Response, error) {
payload := map[string]any{
"MessageType": "DeclarativeManagement",
"UDID": c.UUID,
@ -398,6 +400,13 @@ func (c *TestAppleMDMClient) DeclarativeManagement(endpoint string) (*http.Respo
"EnrollmentID": "testenrollmentid-" + c.UUID,
"Endpoint": endpoint,
}
if len(data) != 0 {
rawData, err := json.Marshal(data[0])
if err != nil {
return nil, fmt.Errorf("marshaling status report: %w", err)
}
payload["Data"] = rawData
}
r, err := c.request("application/x-apple-aspen-mdm-checkin", payload)
return r, err
}

View File

@ -3587,8 +3587,8 @@ func (ds *Datastore) MDMAppleGetHostsWithChangedDeclarations(ctx context.Context
'install' as operation_type,
ds.checksum,
ds.declaration_uuid,
ds.declaration_identifier as identifier,
ds.declaration_name as name
ds.declaration_identifier,
ds.declaration_name
FROM
%s
)
@ -3599,8 +3599,8 @@ func (ds *Datastore) MDMAppleGetHostsWithChangedDeclarations(ctx context.Context
'remove' as operation_type,
hmae.checksum,
hmae.declaration_uuid,
hmae.declaration_identifier as identifier,
hmae.declaration_name as name
hmae.declaration_identifier,
hmae.declaration_name
FROM
%s
)
@ -3615,3 +3615,80 @@ func (ds *Datastore) MDMAppleGetHostsWithChangedDeclarations(ctx context.Context
}
return decls, nil
}
func (ds *Datastore) MDMAppleStoreDDMStatusReport(ctx context.Context, hostUUID string, updates []*fleet.MDMAppleHostDeclaration) error {
getHostDeclarationsStmt := `
SELECT host_uuid, status, operation_type, HEX(checksum) as checksum, declaration_uuid, declaration_identifier, declaration_name
FROM host_mdm_apple_declarations
WHERE host_uuid = ?
`
updateHostDeclarationsStmt := `
INSERT INTO host_mdm_apple_declarations
(host_uuid, declaration_uuid, status, operation_type, detail, declaration_name, declaration_identifier, checksum)
VALUES
%s
ON DUPLICATE KEY UPDATE
status = VALUES(status),
operation_type = VALUES(operation_type),
detail = VALUES(detail)
`
deletePendingRemovesStmt := `
DELETE FROM host_mdm_apple_declarations
WHERE host_uuid = ? AND operation_type = 'remove' AND status = 'pending'
`
var current []*fleet.MDMAppleHostDeclaration
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &current, getHostDeclarationsStmt, hostUUID); err != nil {
return ctxerr.Wrap(ctx, err, "getting current host declarations")
}
updatesByChecksum := make(map[string]*fleet.MDMAppleHostDeclaration, len(updates))
for _, u := range updates {
updatesByChecksum[u.Checksum] = u
}
var args []any
var insertVals strings.Builder
for _, c := range current {
if u, ok := updatesByChecksum[c.Checksum]; ok {
insertVals.WriteString("(?, ?, ?, ?, ?, ?, ?, UNHEX(?)),")
args = append(args, hostUUID, c.DeclarationUUID, u.Status, u.OperationType, u.Detail, c.Identifier, c.Name, c.Checksum)
}
}
err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
if len(args) != 0 {
stmt := fmt.Sprintf(updateHostDeclarationsStmt, strings.TrimSuffix(insertVals.String(), ","))
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "updating existing declarations")
}
}
if _, err := tx.ExecContext(ctx, deletePendingRemovesStmt, hostUUID); err != nil {
return ctxerr.Wrap(ctx, err, "deleting pending removals")
}
return nil
})
return ctxerr.Wrap(ctx, err, "updating host declarations")
}
func (ds *Datastore) MDMAppleSetDeclarationsAsVerifying(ctx context.Context, hostUUID string) error {
stmt := `
UPDATE host_mdm_apple_declarations
SET status = ?
WHERE
operation_type = ?
AND status = ?
AND host_uuid = ?
`
_, err := ds.writer(ctx).ExecContext(
ctx, stmt, fleet.MDMDeliveryVerifying,
fleet.MDMOperationTypeInstall, fleet.MDMDeliveryPending, hostUUID,
)
return ctxerr.Wrap(ctx, err, "updating host declaration status to verifying")
}

View File

@ -635,10 +635,10 @@ type MDMAppleHostDeclaration struct {
DeclarationUUID string `db:"declaration_uuid" json:"profile_uuid"`
// Name corresponds to the file name of the associated JSON declaration payload.
Name string `db:"name" json:"name"`
Name string `db:"declaration_name" json:"name"`
// Identifier corresponds to the "Identifier" key of the associated declaration.
Identifier string `db:"identifier" json:"-"`
Identifier string `db:"declaration_identifier" json:"-"`
// Status represent the current state of the declaration, as known by the Fleet server.
Status *MDMDeliveryStatus `db:"status" json:"status"`
@ -727,3 +727,94 @@ type MDMAppleDDMDeclarationResponse struct {
Payload json.RawMessage `db:"payload"`
ServerToken string `db:"server_token"`
}
// MDMAppleDDMStatusReport represents a report of the device's current state.
//
// https://developer.apple.com/documentation/devicemanagement/statusreport
type MDMAppleDDMStatusReport struct {
StatusItems MDMAppleDDMStatusItems `json:"StatusItems"`
Errors []MDMAppleDDMErrors `json:"Errors"`
}
// MDMAppleDDMStatusItems are the status items for a report.
//
// https://developer.apple.com/documentation/devicemanagement/statusreport/statusitems
type MDMAppleDDMStatusItems struct {
Management MDMAppleDDMStatusManagement `json:"management"`
}
// MDMAppleDDMStatusManagement represents status report of the client's
// processed declarations.
//
// https://developer.apple.com/documentation/devicemanagement/statusmanagementdeclarations
type MDMAppleDDMStatusManagement struct {
Declarations MDMAppleDDMStatusDeclarations `json:"declarations"`
}
// MDMAppleDDMStatusDeclarations represents a collection of the client's
// processed declarations.
//
// https://developer.apple.com/documentation/devicemanagement/statusmanagementdeclarationsdeclarationsobject
type MDMAppleDDMStatusDeclarations struct {
// Activations is an array of declarations that represent the client's
// processed activation types.
Activations []MDMAppleDDMStatusDeclaration `json:"activations"`
// Configurations is an array of declarations that represent the
// client's processed configuration types.
Configurations []MDMAppleDDMStatusDeclaration `json:"configurations"`
// Assets is an array of declarations that represent the client's
// processed assets.
Assets []MDMAppleDDMStatusDeclaration `json:"assets"`
// Management is an array of declarations that represent the client's
// processed declaration types.
Management []MDMAppleDDMStatusDeclaration `json:"management"`
}
type MDMAppleDeclarationValidity string
const (
MDMAppleDeclarationValid MDMAppleDeclarationValidity = "valid"
MDMAppleDeclarationInvalid MDMAppleDeclarationValidity = "invalid"
MDMAppleDeclarationUnknown MDMAppleDeclarationValidity = "valid"
)
// MDMAppleDDMStatusDeclaration represents a processed declaration for the client.
//
// https://developer.apple.com/documentation/devicemanagement/statusmanagementdeclarationsdeclarationobject
type MDMAppleDDMStatusDeclaration struct {
// Active signals if the declaration is active on the device.
Active bool `json:"active"`
// Identifier is the identifier of the declaration this status report refers to.
Identifier string `json:"identifier"`
// Valid defines the validity of the declaration. If it's invalid, the
// reasons property contains more details.
Valid MDMAppleDeclarationValidity `json:"valid"`
// ServerToken of the declaration this status report refers to.
ServerToken string `json:"server-token"`
// Reasons are the details of any client errors.
Reasons []MDMAppleDDMStatusErrorReason `json:"reasons,omitempty"`
}
// A status report's error that contains the status item and the reasons for
// the error.
//
// https://developer.apple.com/documentation/devicemanagement/statusreport/error
type MDMAppleDDMErrors struct {
// StatusItem is the status item that this error pertains to.
StatusItem string `json:"StatusItem"`
// Reasons is an array of reasons for the error.
Reasons []MDMAppleDDMStatusErrorReason `json:"Reasons"`
}
// A status report that contains details about an error.
//
// https://developer.apple.com/documentation/devicemanagement/statusreason
type MDMAppleDDMStatusErrorReason struct {
// Code is the error code for this error.
Code string `json:"Code"`
// Description is a short error description.
Description string `json:"Description"`
// Details is a dictionary that contains further details about this
// error.
Details map[string]any `json:"Details"`
}

View File

@ -1171,6 +1171,16 @@ type Datastore interface {
// MDMAppleBatchInsertHostDeclarations tracks the current status of all
// the host declarations provided.
MDMAppleBatchInsertHostDeclarations(ctx context.Context, changedDeclarations []*MDMAppleHostDeclaration) error
// MDMAppleStoreDDMStatusReport receives a host.uuid and a slice
// of declarations, and updates the tracked host declaration status for
// matching declarations.
//
// It also takes care of cleaning up all host declarations that are
// pending removal.
MDMAppleStoreDDMStatusReport(ctx context.Context, hostUUID string, updates []*MDMAppleHostDeclaration) error
// MDMAppleSetDeclarationsAsVerifying updates all
// ("pending", "install") declarations for a host to be ("verifying", "install")
MDMAppleSetDeclarationsAsVerifying(ctx context.Context, hostUUID string) error
///////////////////////////////////////////////////////////////////////////////
// Microsoft MDM

View File

@ -101,6 +101,7 @@ func GenerateRandomPin(length int) string {
return fmt.Sprintf(f, v)
}
// FmtErrorChain formats Command error message for macOS MDM v1
func FmtErrorChain(chain []mdm.ErrorChain) string {
var sb strings.Builder
for _, mdmErr := range chain {
@ -113,6 +114,15 @@ func FmtErrorChain(chain []mdm.ErrorChain) string {
return sb.String()
}
// FmtDDMError formats a DDM error message
func FmtDDMError(reasons []fleet.MDMAppleDDMStatusErrorReason) string {
var errMsg strings.Builder
for _, r := range reasons {
errMsg.WriteString(fmt.Sprintf("%s: %s %+v\n", r.Code, r.Description, r.Details))
}
return errMsg.String()
}
func EnrollURL(token string, appConfig *fleet.AppConfig) (string, error) {
enrollURL, err := url.Parse(appConfig.ServerSettings.ServerURL)
if err != nil {

View File

@ -768,6 +768,10 @@ type MDMAppleGetHostsWithChangedDeclarationsFunc func(ctx context.Context) ([]*f
type MDMAppleBatchInsertHostDeclarationsFunc func(ctx context.Context, changedDeclarations []*fleet.MDMAppleHostDeclaration) error
type MDMAppleStoreDDMStatusReportFunc func(ctx context.Context, hostUUID string, updates []*fleet.MDMAppleHostDeclaration) error
type MDMAppleSetDeclarationsAsVerifyingFunc func(ctx context.Context, hostUUID string) error
type WSTEPStoreCertificateFunc func(ctx context.Context, name string, crt *x509.Certificate) error
type WSTEPNewSerialFunc func(ctx context.Context) (*big.Int, error)
@ -1998,6 +2002,12 @@ type DataStore struct {
MDMAppleBatchInsertHostDeclarationsFunc MDMAppleBatchInsertHostDeclarationsFunc
MDMAppleBatchInsertHostDeclarationsFuncInvoked bool
MDMAppleStoreDDMStatusReportFunc MDMAppleStoreDDMStatusReportFunc
MDMAppleStoreDDMStatusReportFuncInvoked bool
MDMAppleSetDeclarationsAsVerifyingFunc MDMAppleSetDeclarationsAsVerifyingFunc
MDMAppleSetDeclarationsAsVerifyingFuncInvoked bool
WSTEPStoreCertificateFunc WSTEPStoreCertificateFunc
WSTEPStoreCertificateFuncInvoked bool
@ -4782,6 +4792,20 @@ func (s *DataStore) MDMAppleBatchInsertHostDeclarations(ctx context.Context, cha
return s.MDMAppleBatchInsertHostDeclarationsFunc(ctx, changedDeclarations)
}
func (s *DataStore) MDMAppleStoreDDMStatusReport(ctx context.Context, hostUUID string, updates []*fleet.MDMAppleHostDeclaration) error {
s.mu.Lock()
s.MDMAppleStoreDDMStatusReportFuncInvoked = true
s.mu.Unlock()
return s.MDMAppleStoreDDMStatusReportFunc(ctx, hostUUID, updates)
}
func (s *DataStore) MDMAppleSetDeclarationsAsVerifying(ctx context.Context, hostUUID string) error {
s.mu.Lock()
s.MDMAppleSetDeclarationsAsVerifyingFuncInvoked = true
s.mu.Unlock()
return s.MDMAppleSetDeclarationsAsVerifyingFunc(ctx, hostUUID)
}
func (s *DataStore) WSTEPStoreCertificate(ctx context.Context, name string, crt *x509.Certificate) error {
s.mu.Lock()
s.WSTEPStoreCertificateFuncInvoked = true

View File

@ -2615,7 +2615,13 @@ func (svc *MDMAppleCheckinAndCommandService) CommandAndReportResults(r *mdm.Requ
cmdResult.Status == fleet.MDMAppleStatusCommandFormatError {
return nil, svc.ds.UpdateHostLockWipeStatusFromAppleMDMResult(r.Context, cmdResult.UDID, cmdResult.CommandUUID, requestType, cmdResult.Status == fleet.MDMAppleStatusAcknowledged)
}
case "DeclarativeManagement":
// set "pending-install" profiles to "verifying"
err := svc.ds.MDMAppleSetDeclarationsAsVerifying(r.Context, cmdResult.UDID)
return nil, ctxerr.Wrap(r.Context, err, "update declaration status on DeclarativeManagement ack")
}
return nil, nil
}
@ -2756,6 +2762,8 @@ func ReconcileAppleDeclarations(
return ctxerr.Wrap(ctx, err, "issuing DeclarativeManagement command")
}
logger.Log("msg", "sent DeclarativeManagement command", "host_number", len(uuids))
return nil
}
@ -3244,9 +3252,7 @@ func (svc *MDMAppleDDMService) DeclarativeManagement(r *mdm.Request, dm *mdm.Dec
case dm.Endpoint == "status":
level.Debug(svc.logger).Log("msg", "received status request")
// TODO(roberto): handle status
return nil, nil
return nil, svc.handleDeclarationStatus(r.Context, dm)
case strings.HasPrefix(dm.Endpoint, "declaration/"):
level.Debug(svc.logger).Log("msg", "received declarations request")
@ -3374,3 +3380,56 @@ func (svc *MDMAppleDDMService) handleConfigurationDeclaration(ctx context.Contex
}
return b, nil
}
func (svc *MDMAppleDDMService) handleDeclarationStatus(ctx context.Context, dm *mdm.DeclarativeManagement) error {
var status fleet.MDMAppleDDMStatusReport
if err := json.Unmarshal(dm.Data, &status); err != nil {
return ctxerr.Wrap(ctx, err, "unmarshalling response")
}
configurationReports := status.StatusItems.Management.Declarations.Configurations
updates := make([]*fleet.MDMAppleHostDeclaration, len(configurationReports))
for i, r := range configurationReports {
var status fleet.MDMDeliveryStatus
var detail string
switch {
case r.Active && r.Valid == fleet.MDMAppleDeclarationValid:
status = fleet.MDMDeliveryVerified
case r.Valid == fleet.MDMAppleDeclarationInvalid:
status = fleet.MDMDeliveryFailed
detail = apple_mdm.FmtDDMError(r.Reasons)
default:
status = fleet.MDMDeliveryVerifying
}
updates[i] = &fleet.MDMAppleHostDeclaration{
Status: &status,
OperationType: fleet.MDMOperationTypeInstall,
Detail: detail,
Checksum: r.ServerToken,
}
}
if len(updates) == 0 {
return nil
}
// MDMAppleStoreDDMStatusReport takes care of cleaning ("pending", "remove")
// pairs for the host.
//
// TODO(roberto): in the DDM documentation, it's mentioned that status
// report will give you a "remove" status so the server can track
// removals. In my testing, I never saw this (after spending
// considerable time trying to make it work.)
//
// My current guess is that the documentation is implicitly referring
// to asset declarations (which deliver tangible "assets" to the host)
//
// The best indication I found so far, is that if the declaration is
// not in the report, then it's implicitly removed.
if err := svc.ds.MDMAppleStoreDDMStatusReport(ctx, dm.UDID, updates); err != nil {
return ctxerr.Wrap(ctx, err, "updating host declaration status with reports")
}
return nil
}

View File

@ -12982,7 +12982,7 @@ INSERT INTO host_mdm_apple_declarations (
})
t.Run("Status", func(t *testing.T) {
_, err := mdmDevice.DeclarativeManagement("status")
_, err := mdmDevice.DeclarativeManagement("status", fleet.MDMAppleDDMStatusReport{})
require.NoError(t, err)
})
@ -13266,6 +13266,144 @@ func (s *integrationMDMTestSuite) TestAppleDDMReconciliation() {
checkDDMSync(deviceThree)
}
func (s *integrationMDMTestSuite) TestAppleDDMStatusReport() {
t := s.T()
ctx := context.Background()
// TODO: use config logger or take into account FLEET_INTEGRATION_TESTS_DISABLE_LOG
logger := kitlog.NewJSONLogger(os.Stdout)
assertHostDeclarations := func(hostUUID string, wantDecls []*fleet.MDMAppleHostDeclaration) {
var gotDecls []*fleet.MDMAppleHostDeclaration
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
return sqlx.SelectContext(context.Background(), q, &gotDecls, `SELECT declaration_identifier, status, operation_type FROM host_mdm_apple_declarations WHERE host_uuid = ?`, hostUUID)
})
require.ElementsMatch(t, wantDecls, gotDecls)
}
// create a host and then enroll in MDM.
mdmHost, device := createHostThenEnrollMDM(s.ds, s.server.URL, t)
declarations := []fleet.MDMProfileBatchPayload{
{Name: "N1.json", Contents: declarationForTest("I1")},
{Name: "N2.json", Contents: declarationForTest("I2")},
}
// add global declarations
s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: declarations}, http.StatusNoContent)
// reconcile profiles
err := ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
require.NoError(t, err)
// declarations are ("install", "pending") after the cron run
assertHostDeclarations(mdmHost.UUID, []*fleet.MDMAppleHostDeclaration{
{Identifier: "I1", Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall},
{Identifier: "I2", Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall},
})
// host gets a DDM sync call
cmd, err := device.Idle()
require.NoError(t, err)
require.Equal(t, "DeclarativeManagement", cmd.Command.RequestType)
_, err = device.Acknowledge(cmd.CommandUUID)
require.NoError(t, err)
r, err := device.DeclarativeManagement("declaration-items")
require.NoError(t, err)
body, err := io.ReadAll(r.Body)
require.NoError(t, err)
var items fleet.MDMAppleDDMDeclarationItemsResponse
require.NoError(t, json.Unmarshal(body, &items))
var i1ServerToken, i2ServerToken string
for _, d := range items.Declarations.Configurations {
switch d.Identifier {
case "I1":
i1ServerToken = d.ServerToken
case "I2":
i2ServerToken = d.ServerToken
}
}
// declarations are ("install", "verifying") after the ack
assertHostDeclarations(mdmHost.UUID, []*fleet.MDMAppleHostDeclaration{
{Identifier: "I1", Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall},
{Identifier: "I2", Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall},
})
// host sends a partial DDM report
report := fleet.MDMAppleDDMStatusReport{}
report.StatusItems.Management.Declarations.Configurations = []fleet.MDMAppleDDMStatusDeclaration{
{Active: true, Valid: fleet.MDMAppleDeclarationValid, Identifier: "I1", ServerToken: i1ServerToken},
}
_, err = device.DeclarativeManagement("status", report)
require.NoError(t, err)
assertHostDeclarations(mdmHost.UUID, []*fleet.MDMAppleHostDeclaration{
{Identifier: "I1", Status: &fleet.MDMDeliveryVerified, OperationType: fleet.MDMOperationTypeInstall},
{Identifier: "I2", Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall},
})
// host sends a report with a wrong (could be old) server token for I2, nothing changes
report = fleet.MDMAppleDDMStatusReport{}
report.StatusItems.Management.Declarations.Configurations = []fleet.MDMAppleDDMStatusDeclaration{
{Active: true, Valid: fleet.MDMAppleDeclarationValid, Identifier: "I2", ServerToken: "foo"},
}
_, err = device.DeclarativeManagement("status", report)
require.NoError(t, err)
assertHostDeclarations(mdmHost.UUID, []*fleet.MDMAppleHostDeclaration{
{Identifier: "I1", Status: &fleet.MDMDeliveryVerified, OperationType: fleet.MDMOperationTypeInstall},
{Identifier: "I2", Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall},
})
// host sends a full report, declaration I2 is invalid
report = fleet.MDMAppleDDMStatusReport{}
report.StatusItems.Management.Declarations.Configurations = []fleet.MDMAppleDDMStatusDeclaration{
{Active: true, Valid: fleet.MDMAppleDeclarationValid, Identifier: "I1", ServerToken: i1ServerToken},
{Active: false, Valid: fleet.MDMAppleDeclarationInvalid, Identifier: "I2", ServerToken: i2ServerToken},
}
_, err = device.DeclarativeManagement("status", report)
require.NoError(t, err)
assertHostDeclarations(mdmHost.UUID, []*fleet.MDMAppleHostDeclaration{
{Identifier: "I1", Status: &fleet.MDMDeliveryVerified, OperationType: fleet.MDMOperationTypeInstall},
{Identifier: "I2", Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeInstall},
})
// do a batch request, this time I2 is deleted
declarations = []fleet.MDMProfileBatchPayload{
{Name: "N1.json", Contents: declarationForTest("I1")},
}
s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: declarations}, http.StatusNoContent)
// reconcile profiles
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
require.NoError(t, err)
assertHostDeclarations(mdmHost.UUID, []*fleet.MDMAppleHostDeclaration{
{Identifier: "I1", Status: &fleet.MDMDeliveryVerified, OperationType: fleet.MDMOperationTypeInstall},
{Identifier: "I2", Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeRemove},
})
// host sends a report, declaration I2 is removed from the hosts_* table
report = fleet.MDMAppleDDMStatusReport{}
report.StatusItems.Management.Declarations.Configurations = []fleet.MDMAppleDDMStatusDeclaration{
{Active: true, Valid: fleet.MDMAppleDeclarationValid, Identifier: "I1", ServerToken: i1ServerToken},
}
_, err = device.DeclarativeManagement("status", report)
require.NoError(t, err)
assertHostDeclarations(mdmHost.UUID, []*fleet.MDMAppleHostDeclaration{
{Identifier: "I1", Status: &fleet.MDMDeliveryVerified, OperationType: fleet.MDMOperationTypeInstall},
})
// host sends a report, declaration I1 is failing after a while
report = fleet.MDMAppleDDMStatusReport{}
report.StatusItems.Management.Declarations.Configurations = []fleet.MDMAppleDDMStatusDeclaration{
{Active: false, Valid: fleet.MDMAppleDeclarationInvalid, Identifier: "I1", ServerToken: i1ServerToken},
}
_, err = device.DeclarativeManagement("status", report)
require.NoError(t, err)
assertHostDeclarations(mdmHost.UUID, []*fleet.MDMAppleHostDeclaration{
{Identifier: "I1", Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeInstall},
})
}
func declarationForTest(identifier string) []byte {
return []byte(fmt.Sprintf(`
{