diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go index 2d2e267647..58a1966a9f 100644 --- a/server/datastore/mysql/apple_mdm.go +++ b/server/datastore/mysql/apple_mdm.go @@ -3082,59 +3082,64 @@ WHERE h.uuid = ? return nil } -func (ds *Datastore) MDMAppleDDMSynchronizationTokens(ctx context.Context, teamID uint) (*fleet.MDMAppleDDMSyncTokens, error) { +func (ds *Datastore) MDMAppleDDMDeclarationsToken(ctx context.Context, hostUUID string) (*fleet.MDMAppleDDMDeclarationsToken, error) { const stmt = ` SELECT - md5_checksum, - latest_created_timestamp + md5((count(0) + group_concat(hex(mad.md5_checksum) + ORDER BY + mad.uploaded_at DESC separator ''))) AS md5_checksum, + max(mad.created_at) AS latest_created_timestamp FROM - team_declaration_checksum_view + host_mdm_apple_declarations hmad + JOIN mdm_apple_declarations mad ON hmad.declaration_uuid = mad.declaration_uuid WHERE - team_id = ?` + hmad.host_uuid = ?` - var res fleet.MDMAppleDDMSyncTokens - if err := sqlx.GetContext(ctx, ds.reader(ctx), &res, stmt, teamID); err != nil { - return nil, ctxerr.Wrap(ctx, err, "get DDM checksum by team id") + var res fleet.MDMAppleDDMDeclarationsToken + if err := sqlx.GetContext(ctx, ds.reader(ctx), &res, stmt, hostUUID); err != nil { + return nil, ctxerr.Wrap(ctx, err, "get DDM declarations token") } return &res, nil } -func (ds *Datastore) MDMAppleDDMDeclarationItems(ctx context.Context, teamID uint) ([]fleet.MDMAppleDDMDeclarationItem, error) { - // TODO: Confirm whether we can use JSON functions in the query (if 5.7 officially unsupported - // by Fleet) +func (ds *Datastore) MDMAppleDDMDeclarationItems(ctx context.Context, hostUUID string) ([]fleet.MDMAppleDDMDeclarationItem, error) { const stmt = ` SELECT - mad.md5_checksum as server_token, - identifier, - declaration_type, - tv.md5_checksum as declarations_token + mad.md5_checksum, + mad.identifier, + mad.declaration_type FROM - mdm_apple_declarations mad - JOIN team_declaration_checksum_view tv ON mad.team_id = tv.team_id + host_mdm_apple_declarations hmad + JOIN mdm_apple_declarations mad ON mad.declaration_uuid = hmad.declaration_uuid WHERE - mad.team_id = ?` + hmad.host_uuid = ? AND operation_type = ?` var res []fleet.MDMAppleDDMDeclarationItem - if err := sqlx.SelectContext(ctx, ds.reader(ctx), &res, stmt, teamID); err != nil { - return nil, ctxerr.Wrap(ctx, err, "get DDM checksum by team id") + if err := sqlx.SelectContext(ctx, ds.reader(ctx), &res, stmt, hostUUID, fleet.MDMOperationTypeInstall); err != nil { + return nil, ctxerr.Wrap(ctx, err, "get DDM declaration items") } return res, nil } -func (ds *Datastore) MDMAppleDDMDeclarationPayload(ctx context.Context, declarationType fleet.MDMAppleDeclarationType, identifier string, teamID uint) (json.RawMessage, error) { +func (ds *Datastore) MDMAppleDDMDeclarationsResponse(ctx context.Context, declarationType fleet.MDMAppleDeclarationType, identifier string, hostUUID string) (json.RawMessage, error) { + // TODO: When hosts table is indexed by uuid, consider joining on hosts to ensure that the + // declaration for the host's current team is returned. In the case where the specified + // identifier is not unique to the team, the cron should ensure that any conflicting + // declarations are removed, but the join would provide an extra layer of safety. const stmt = ` SELECT - declaration + mad.declaration FROM - mdm_apple_declarations + host_mdm_apple_declarations hmad + JOIN mdm_apple_declarations mad ON hmad.declaration_uuid = mad.declaration_uuid WHERE - team_id = ? AND identifier = ? AND declaration_type = ?` + host_uuid = ? AND identifier = ? AND declaration_type = ? AND operation_type = ?` var res json.RawMessage - if err := sqlx.GetContext(ctx, ds.reader(ctx), &res, stmt, teamID, identifier, declarationType); err != nil { - return nil, ctxerr.Wrap(ctx, err, "get ddm declaration") + if err := sqlx.GetContext(ctx, ds.reader(ctx), &res, stmt, hostUUID, identifier, declarationType, fleet.MDMOperationTypeInstall); err != nil { + return nil, ctxerr.Wrap(ctx, err, "get ddm declarations response") } return res, nil diff --git a/server/fleet/apple_mdm.go b/server/fleet/apple_mdm.go index 95a2bad009..56116daf6d 100644 --- a/server/fleet/apple_mdm.go +++ b/server/fleet/apple_mdm.go @@ -614,13 +614,13 @@ type MDMAppleHostDeclaration struct { // // https://developer.apple.com/documentation/devicemanagement/tokensresponse type MDMAppleDDMTokensResponse struct { - SyncTokens MDMAppleDDMSyncTokens + SyncTokens MDMAppleDDMDeclarationsToken } -// MDMAppleDDMSyncTokens is dictionary describes the state of declarations on the server. +// MDMAppleDDMDeclarationsToken is dictionary describes the state of declarations on the server. // // https://developer.apple.com/documentation/devicemanagement/synchronizationtokens -type MDMAppleDDMSyncTokens struct { +type MDMAppleDDMDeclarationsToken struct { DeclarationsToken string `db:"md5_checksum"` Timestamp time.Time `db:"latest_created_timestamp"` } @@ -657,10 +657,9 @@ type MDMAppleDDMManifest struct { // // https://developer.apple.com/documentation/devicemanagement/declarationitemsresponse type MDMAppleDDMDeclarationItem struct { - Identifier string `db:"identifier"` - DeclarationType string `db:"declaration_type"` - DeclarationsToken string `db:"declarations_token"` - ServerToken string `db:"server_token"` + Identifier string `db:"identifier"` + DeclarationType string `db:"declaration_type"` + ServerToken string `db:"md5_checksum"` } // MDMAppleDDMDeclarationResponse represents a declaration in the datastore. It is used for the DDM diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 61a590ac2c..aac24814e9 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -1157,13 +1157,13 @@ type Datastore interface { // serials. UpdateDEPAssignProfileRetryPending(ctx context.Context, jobID uint, serials []string) error - // MDMAppleDDMSynchronizationTokens returns the token used to synchronize declarations for the - // specified team or no team. - MDMAppleDDMSynchronizationTokens(ctx context.Context, teamID uint) (*MDMAppleDDMSyncTokens, error) - // MDMAppleDDMDeclarationItems returns the declaration items for the specified team or no team. - MDMAppleDDMDeclarationItems(ctx context.Context, teamID uint) ([]MDMAppleDDMDeclarationItem, error) + // MDMAppleDDMDeclarationsToken returns the token used to synchronize declarations for the + // specified host UUID. + MDMAppleDDMDeclarationsToken(ctx context.Context, hostUUID string) (*MDMAppleDDMDeclarationsToken, error) + // MDMAppleDDMDeclarationItems returns the declaration items for the specified host UUID. + MDMAppleDDMDeclarationItems(ctx context.Context, hostUUID string) ([]MDMAppleDDMDeclarationItem, error) // MDMAppleDDMDeclarationPayload returns the declaration payload for the specified identifier and team. - MDMAppleDDMDeclarationPayload(ctx context.Context, declarationType MDMAppleDeclarationType, identifier string, teamID uint) (json.RawMessage, error) + MDMAppleDDMDeclarationsResponse(ctx context.Context, declarationType MDMAppleDeclarationType, identifier string, hostUUID string) (json.RawMessage, error) /////////////////////////////////////////////////////////////////////////////// // Microsoft MDM diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index a7e1f8df11..4e6311ed01 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -758,11 +758,11 @@ type GetDEPAssignProfileExpiredCooldownsFunc func(ctx context.Context) (map[uint type UpdateDEPAssignProfileRetryPendingFunc func(ctx context.Context, jobID uint, serials []string) error -type MDMAppleDDMSynchronizationTokensFunc func(ctx context.Context, teamID uint) (*fleet.MDMAppleDDMSyncTokens, error) +type MDMAppleDDMDeclarationsTokenFunc func(ctx context.Context, hostUUID string) (*fleet.MDMAppleDDMDeclarationsToken, error) -type MDMAppleDDMDeclarationItemsFunc func(ctx context.Context, teamID uint) ([]fleet.MDMAppleDDMDeclarationItem, error) +type MDMAppleDDMDeclarationItemsFunc func(ctx context.Context, hostUUID string) ([]fleet.MDMAppleDDMDeclarationItem, error) -type MDMAppleDDMDeclarationPayloadFunc func(ctx context.Context, declarationType fleet.MDMAppleDeclarationType, identifier string, teamID uint) (json.RawMessage, error) +type MDMAppleDDMDeclarationsResponseFunc func(ctx context.Context, declarationType fleet.MDMAppleDeclarationType, identifier string, hostUUID string) (json.RawMessage, error) type WSTEPStoreCertificateFunc func(ctx context.Context, name string, crt *x509.Certificate) error @@ -1977,14 +1977,14 @@ type DataStore struct { UpdateDEPAssignProfileRetryPendingFunc UpdateDEPAssignProfileRetryPendingFunc UpdateDEPAssignProfileRetryPendingFuncInvoked bool - MDMAppleDDMSynchronizationTokensFunc MDMAppleDDMSynchronizationTokensFunc - MDMAppleDDMSynchronizationTokensFuncInvoked bool + MDMAppleDDMDeclarationsTokenFunc MDMAppleDDMDeclarationsTokenFunc + MDMAppleDDMDeclarationsTokenFuncInvoked bool MDMAppleDDMDeclarationItemsFunc MDMAppleDDMDeclarationItemsFunc MDMAppleDDMDeclarationItemsFuncInvoked bool - MDMAppleDDMDeclarationPayloadFunc MDMAppleDDMDeclarationPayloadFunc - MDMAppleDDMDeclarationPayloadFuncInvoked bool + MDMAppleDDMDeclarationsResponseFunc MDMAppleDDMDeclarationsResponseFunc + MDMAppleDDMDeclarationsResponseFuncInvoked bool WSTEPStoreCertificateFunc WSTEPStoreCertificateFunc WSTEPStoreCertificateFuncInvoked bool @@ -4732,25 +4732,25 @@ func (s *DataStore) UpdateDEPAssignProfileRetryPending(ctx context.Context, jobI return s.UpdateDEPAssignProfileRetryPendingFunc(ctx, jobID, serials) } -func (s *DataStore) MDMAppleDDMSynchronizationTokens(ctx context.Context, teamID uint) (*fleet.MDMAppleDDMSyncTokens, error) { +func (s *DataStore) MDMAppleDDMDeclarationsToken(ctx context.Context, hostUUID string) (*fleet.MDMAppleDDMDeclarationsToken, error) { s.mu.Lock() - s.MDMAppleDDMSynchronizationTokensFuncInvoked = true + s.MDMAppleDDMDeclarationsTokenFuncInvoked = true s.mu.Unlock() - return s.MDMAppleDDMSynchronizationTokensFunc(ctx, teamID) + return s.MDMAppleDDMDeclarationsTokenFunc(ctx, hostUUID) } -func (s *DataStore) MDMAppleDDMDeclarationItems(ctx context.Context, teamID uint) ([]fleet.MDMAppleDDMDeclarationItem, error) { +func (s *DataStore) MDMAppleDDMDeclarationItems(ctx context.Context, hostUUID string) ([]fleet.MDMAppleDDMDeclarationItem, error) { s.mu.Lock() s.MDMAppleDDMDeclarationItemsFuncInvoked = true s.mu.Unlock() - return s.MDMAppleDDMDeclarationItemsFunc(ctx, teamID) + return s.MDMAppleDDMDeclarationItemsFunc(ctx, hostUUID) } -func (s *DataStore) MDMAppleDDMDeclarationPayload(ctx context.Context, declarationType fleet.MDMAppleDeclarationType, identifier string, teamID uint) (json.RawMessage, error) { +func (s *DataStore) MDMAppleDDMDeclarationsResponse(ctx context.Context, declarationType fleet.MDMAppleDeclarationType, identifier string, hostUUID string) (json.RawMessage, error) { s.mu.Lock() - s.MDMAppleDDMDeclarationPayloadFuncInvoked = true + s.MDMAppleDDMDeclarationsResponseFuncInvoked = true s.mu.Unlock() - return s.MDMAppleDDMDeclarationPayloadFunc(ctx, declarationType, identifier, teamID) + return s.MDMAppleDDMDeclarationsResponseFunc(ctx, declarationType, identifier, hostUUID) } func (s *DataStore) WSTEPStoreCertificate(ctx context.Context, name string, crt *x509.Certificate) error { diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 77e94f2920..40720ce820 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -2978,15 +2978,6 @@ func (svc *MDMAppleDDMService) DeclarativeManagement(r *mdm.Request, dm *mdm.Dec return nil, ctxerr.New(r.Context, "missing device id") } - h, err := svc.ds.HostLiteByIdentifier(r.Context, dm.UDID) - if err != nil { - return nil, ctxerr.Wrap(r.Context, err, "getting host by identifier") - } - var tid uint - if h.TeamID != nil { - tid = *h.TeamID - } - switch { case dm.Endpoint == "tokens": level.Debug(svc.logger).Log("msg", "received tokens request") @@ -2995,11 +2986,11 @@ func (svc *MDMAppleDDMService) DeclarativeManagement(r *mdm.Request, dm *mdm.Dec return nil, ctxerr.Wrap(r.Context, err, "recording declarative checkin") } - return svc.handleTokens(r.Context, tid) + return svc.handleTokens(r.Context, dm.UDID) case dm.Endpoint == "declaration-items": level.Debug(svc.logger).Log("msg", "received declaration-items request") - return svc.handleDeclarationItems(r.Context, tid) + return svc.handleDeclarationItems(r.Context, dm.UDID) case dm.Endpoint == "status": level.Debug(svc.logger).Log("msg", "received status request") @@ -3009,32 +3000,15 @@ func (svc *MDMAppleDDMService) DeclarativeManagement(r *mdm.Request, dm *mdm.Dec case strings.HasPrefix(dm.Endpoint, "declarations"): level.Debug(svc.logger).Log("msg", "received declarations request") - parts := strings.Split(dm.Endpoint, "/") - if len(parts) != 3 { - return nil, ctxerr.New(r.Context, "unrecognized declarations endpoint") - } - declarationType := parts[1] - declarationIdentifier := parts[2] - level.Debug(svc.logger).Log("msg", "parsed declarations request", "type", declarationType, "identifier", declarationIdentifier) - - // TODO: Validate declarationType? - d, err := svc.ds.MDMAppleDDMDeclarationPayload(r.Context, fleet.MDMAppleDeclarationType("com.apple."+declarationType), declarationIdentifier, tid) - if err != nil { - return nil, ctxerr.Wrap(r.Context, err, "getting declaration") - } - b, err := json.Marshal(d) - if err != nil { - return nil, ctxerr.Wrap(r.Context, err, "marshaling declaration") - } - return b, nil + return svc.handleDeclarationsResponse(r.Context, dm.Endpoint, dm.UDID) default: return nil, ctxerr.New(r.Context, "unrecognized ddm endpoint") } } -func (svc *MDMAppleDDMService) handleTokens(ctx context.Context, teamID uint) ([]byte, error) { - tok, err := svc.ds.MDMAppleDDMSynchronizationTokens(ctx, teamID) +func (svc *MDMAppleDDMService) handleTokens(ctx context.Context, hostUUID string) ([]byte, error) { + tok, err := svc.ds.MDMAppleDDMDeclarationsToken(ctx, hostUUID) if err != nil { return nil, ctxerr.Wrap(ctx, err, "getting synchronization tokens") } @@ -3049,22 +3023,15 @@ func (svc *MDMAppleDDMService) handleTokens(ctx context.Context, teamID uint) ([ return b, nil } -func (svc *MDMAppleDDMService) handleDeclarationItems(ctx context.Context, teamID uint) ([]byte, error) { - di, err := svc.ds.MDMAppleDDMDeclarationItems(ctx, teamID) +func (svc *MDMAppleDDMService) handleDeclarationItems(ctx context.Context, hostUUID string) ([]byte, error) { + di, err := svc.ds.MDMAppleDDMDeclarationItems(ctx, hostUUID) if err != nil { return nil, ctxerr.Wrap(ctx, err, "getting synchronization tokens") } - var dTok string activations := []fleet.MDMAppleDDMManifest{} configurations := []fleet.MDMAppleDDMManifest{} for _, d := range di { - if dTok == "" { - dTok = d.DeclarationsToken - } else if dTok != d.DeclarationsToken { - level.Debug(svc.logger).Log("msg", "inconsistent declarations token", "expected", dTok, "got", d.DeclarationsToken) - } - manifest := fleet.MDMAppleDDMManifest{Identifier: d.Identifier, ServerToken: d.ServerToken} switch d.DeclarationType { case string(fleet.MDMAppleDeclarativeActivation): @@ -3077,6 +3044,12 @@ func (svc *MDMAppleDDMService) handleDeclarationItems(ctx context.Context, teamI } } + // TODO: Look for ways to optimize the declaration item query so that we don't have to get the declarations token separately. + dTok, err := svc.ds.MDMAppleDDMDeclarationsToken(ctx, hostUUID) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "getting declarations token") + } + b, err := json.Marshal(fleet.MDMAppleDDMDeclarationItemsResponse{ Declarations: fleet.MDMAppleDDMManifestItems{ Activations: activations, @@ -3084,7 +3057,7 @@ func (svc *MDMAppleDDMService) handleDeclarationItems(ctx context.Context, teamI Assets: []fleet.MDMAppleDDMManifest{}, Management: []fleet.MDMAppleDDMManifest{}, }, - DeclarationsToken: dTok, + DeclarationsToken: dTok.DeclarationsToken, }) if err != nil { return nil, ctxerr.Wrap(ctx, err, "marshaling synchronization tokens") @@ -3092,3 +3065,23 @@ func (svc *MDMAppleDDMService) handleDeclarationItems(ctx context.Context, teamI return b, nil } + +func (svc *MDMAppleDDMService) handleDeclarationsResponse(ctx context.Context, endpoint string, hostUUID string) ([]byte, error) { + parts := strings.Split(endpoint, "/") + if len(parts) != 3 { + return nil, ctxerr.New(ctx, "unrecognized declarations endpoint") + } + level.Debug(svc.logger).Log("msg", "parsed declarations request", "type", parts[1], "identifier", parts[2]) + + // TODO: Validate declarationType? + d, err := svc.ds.MDMAppleDDMDeclarationsResponse(ctx, fleet.MDMAppleDeclarationType("com.apple."+parts[1]), parts[2], hostUUID) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "getting declaration") + } + b, err := json.Marshal(d) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "marshaling declaration") + } + + return b, nil +} diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index bdb8f3f2a7..e22e32c217 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -12519,7 +12519,6 @@ func (s *integrationMDMTestSuite) TestIsServerBitlockerStatus() { require.Equal(t, fleet.DiskEncryptionEnforcing, *hr.Host.MDM.OSSettings.DiskEncryption.Status) } -// TODO(sarah): Build out this test func (s *integrationMDMTestSuite) TestMDMAppleDeviceManagementRequests() { t := s.T() _, mdmDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t) @@ -12554,6 +12553,28 @@ INSERT INTO mdm_apple_declarations ( }) } + insertHostDeclaration := func(t *testing.T, hostUUID string, decl fleet.MDMAppleDeclaration) { + stmt := ` +INSERT INTO host_mdm_apple_declarations ( + host_uuid, + status, + operation_type, + md5_checksum, + declaration_uuid +) VALUES (?,?,?,?,?)` + + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(context.Background(), stmt, + hostUUID, + fleet.MDMDeliveryPending, + fleet.MDMOperationTypeInstall, + decl.MD5Checksum, + decl.DeclarationUUID, + ) + return err + }) + } + // initialize a time to use for our first declaration, subsequent declarations will be // incremented by a minute then := time.Now().UTC().Truncate(time.Second).Add(-1 * time.Hour) @@ -12573,6 +12594,7 @@ INSERT INTO mdm_apple_declarations ( }, } insertDeclaration(t, noTeamDeclsByUUID["123"]) + insertHostDeclaration(t, mdmDevice.UUID, noTeamDeclsByUUID["123"]) mapDeclsByChecksum := func(byUUID map[string]fleet.MDMAppleDeclaration) map[string]fleet.MDMAppleDeclaration { byChecksum := make(map[string]fleet.MDMAppleDeclaration) @@ -12680,6 +12702,7 @@ INSERT INTO mdm_apple_declarations ( UploadedAt: then.Add(1 * time.Minute), } insertDeclaration(t, noTeamDeclsByUUID["456"]) + insertHostDeclaration(t, mdmDevice.UUID, noTeamDeclsByUUID["456"]) // get tokens again, timestamp and token should have changed r, err = mdmDevice.DeclarativeManagement("tokens") @@ -12707,6 +12730,7 @@ INSERT INTO mdm_apple_declarations ( UploadedAt: then.Add(2 * time.Minute), } insertDeclaration(t, noTeamDeclsByUUID["789"]) + insertHostDeclaration(t, mdmDevice.UUID, noTeamDeclsByUUID["789"]) // get tokens again, timestamp and token should have changed r, err = mdmDevice.DeclarativeManagement("tokens") @@ -12752,6 +12776,7 @@ INSERT INTO mdm_apple_declarations ( UploadedAt: then.Add(3 * time.Minute), } insertDeclaration(t, noTeamDeclsByUUID["abc"]) + insertHostDeclaration(t, mdmDevice.UUID, noTeamDeclsByUUID["abc"]) want = noTeamDeclsByUUID["abc"] wantBytes, err = json.Marshal(want.Declaration) require.NoError(t, err)