From f0ad942a57c96fde9929478b444412cd683ee2a6 Mon Sep 17 00:00:00 2001 From: Roberto Dip Date: Tue, 26 Mar 2024 10:40:35 -0300 Subject: [PATCH] implement status reports for DDM commands (#17831) for #17408 --- pkg/mdm/mdmtest/apple.go | 11 +- server/datastore/mysql/apple_mdm.go | 85 ++++++++++++++- server/fleet/apple_mdm.go | 95 ++++++++++++++++- server/fleet/datastore.go | 10 ++ server/mdm/apple/util.go | 10 ++ server/mock/datastore_mock.go | 24 +++++ server/service/apple_mdm.go | 65 +++++++++++- server/service/integration_mdm_test.go | 140 ++++++++++++++++++++++++- 8 files changed, 429 insertions(+), 11 deletions(-) diff --git a/pkg/mdm/mdmtest/apple.go b/pkg/mdm/mdmtest/apple.go index be3759ca6..deb52664b 100644 --- a/pkg/mdm/mdmtest/apple.go +++ b/pkg/mdm/mdmtest/apple.go @@ -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 } diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go index 8432c068b..bf80e95f6 100644 --- a/server/datastore/mysql/apple_mdm.go +++ b/server/datastore/mysql/apple_mdm.go @@ -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), ¤t, 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") +} diff --git a/server/fleet/apple_mdm.go b/server/fleet/apple_mdm.go index 093656e58..88ea0f059 100644 --- a/server/fleet/apple_mdm.go +++ b/server/fleet/apple_mdm.go @@ -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"` +} diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index e19f7e0b9..fbd863679 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -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 diff --git a/server/mdm/apple/util.go b/server/mdm/apple/util.go index 801ca37d6..bc8b2bc6e 100644 --- a/server/mdm/apple/util.go +++ b/server/mdm/apple/util.go @@ -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 { diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 7ab7c83a0..bb7ae7c5f 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -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 diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index e64311c95..7ddd09273 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -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 +} diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 62039380a..e88d610ed 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -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(` {