From bd51e858acb29fc980218c20204bc3876e8956cc Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Mon, 30 Dec 2024 17:58:39 -0600 Subject: [PATCH] Update Apple config/DDM profiles if secret variables changed (#24995) #24900 This PR includes and depends on PR #25012, which should be reviewed/merged before this one. Windows profiles are not included in this PR due to issue #25030 This PR adds the following functionality: Apple config/DDM profile is resent to the device when the profile contains secret variables, and the values of those variables have changed. For example. - Upload secret variables - Upload profile - Device gets profile - Upload the same profile - Nothing happens - Upload a different secret variable value - Upload the same profile - Device gets updated profile # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files) for more information. - [x] Added/updated tests - [x] If database migrations are included, checked table schema to confirm autoupdate - For database migrations: - [x] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [x] Manual QA for all new/changed functionality --- changes/23238-use-secrets-in-scripts-profiles | 1 + cmd/fleetctl/apply_test.go | 8 +- cmd/fleetctl/gitops_test.go | 12 +- server/datastore/mysql/apple_mdm.go | 205 +++++++++--------- server/datastore/mysql/apple_mdm_test.go | 6 +- server/datastore/mysql/mdm.go | 4 + server/datastore/mysql/microsoft_mdm.go | 1 + .../20241230000000_AddSecretsUpdatedAt.go | 54 +++++ server/datastore/mysql/schema.sql | 19 +- server/datastore/mysql/secret_variables.go | 105 +++++++-- .../datastore/mysql/secret_variables_test.go | 35 ++- server/fleet/apple_mdm.go | 41 ++-- server/fleet/apple_mdm_test.go | 81 +------ server/fleet/datastore.go | 4 + server/fleet/mdm.go | 9 +- server/fleet/secret_variables.go | 7 +- server/mock/datastore_mock.go | 14 +- server/service/apple_mdm.go | 19 +- server/service/apple_mdm_test.go | 3 + server/service/integration_mdm_ddm_test.go | 137 +++++++++--- .../service/integration_mdm_profiles_test.go | 82 ++++++- server/service/mdm.go | 5 +- server/service/mdm_test.go | 8 +- 23 files changed, 568 insertions(+), 292 deletions(-) create mode 100644 server/datastore/mysql/migrations/tables/20241230000000_AddSecretsUpdatedAt.go diff --git a/changes/23238-use-secrets-in-scripts-profiles b/changes/23238-use-secrets-in-scripts-profiles index ae55893da731..4df69e0d13cc 100644 --- a/changes/23238-use-secrets-in-scripts-profiles +++ b/changes/23238-use-secrets-in-scripts-profiles @@ -2,3 +2,4 @@ Added ability to use secrets ($FLEET_SECRET_YOURNAME) in scripts and profiles. - Added `/fleet/spec/secret_variables` API endpoint. - fleetctl gitops identifies secrets in scripts and profiles and saves them on the Fleet server. - secret values are populated when scripts and profiles are sent to devices. +- When fleetctl gitops updates profiles, if the secret value has changed, the profile is updated on the host. diff --git a/cmd/fleetctl/apply_test.go b/cmd/fleetctl/apply_test.go index 8369058eb1d1..8b671ecbc18c 100644 --- a/cmd/fleetctl/apply_test.go +++ b/cmd/fleetctl/apply_test.go @@ -208,8 +208,8 @@ func TestApplyTeamSpecs(t *testing.T) { ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error { return nil } - ds.ExpandEmbeddedSecretsFunc = func(ctx context.Context, document string) (string, error) { - return document, nil + ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, document string) (string, *time.Time, error) { + return document, nil, nil } filename := writeTmpYml(t, ` @@ -1362,8 +1362,8 @@ func TestApplyAsGitOps(t *testing.T) { ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) { return []*fleet.VPPTokenDB{}, nil } - ds.ExpandEmbeddedSecretsFunc = func(ctx context.Context, document string) (string, error) { - return document, nil + ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, document string) (string, *time.Time, error) { + return document, nil, nil } // Apply global config. diff --git a/cmd/fleetctl/gitops_test.go b/cmd/fleetctl/gitops_test.go index 93d85eff0439..c5e5cde15f83 100644 --- a/cmd/fleetctl/gitops_test.go +++ b/cmd/fleetctl/gitops_test.go @@ -660,8 +660,8 @@ func TestGitOpsFullGlobal(t *testing.T) { return []*fleet.ABMToken{}, nil } - ds.ExpandEmbeddedSecretsFunc = func(ctx context.Context, document string) (string, error) { - return document, nil + ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, document string) (string, *time.Time, error) { + return document, nil, nil } const ( @@ -865,8 +865,8 @@ func TestGitOpsFullTeam(t *testing.T) { return nil } - ds.ExpandEmbeddedSecretsFunc = func(ctx context.Context, document string) (string, error) { - return document, nil + ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, document string) (string, *time.Time, error) { + return document, nil, nil } // Queries @@ -2599,8 +2599,8 @@ func setupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig, ds.SetSetupExperienceScriptFunc = func(ctx context.Context, script *fleet.Script) error { return nil } - ds.ExpandEmbeddedSecretsFunc = func(ctx context.Context, document string) (string, error) { - return document, nil + ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, document string) (string, *time.Time, error) { + return document, nil, nil } t.Setenv("FLEET_SERVER_URL", fleetServerURL) diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go index 578d5c7f7edb..7e3dc61158e7 100644 --- a/server/datastore/mysql/apple_mdm.go +++ b/server/datastore/mysql/apple_mdm.go @@ -39,8 +39,8 @@ func (ds *Datastore) NewMDMAppleConfigProfile(ctx context.Context, cp fleet.MDMA profUUID := "a" + uuid.New().String() stmt := ` INSERT INTO - mdm_apple_configuration_profiles (profile_uuid, team_id, identifier, name, mobileconfig, checksum, uploaded_at) -(SELECT ?, ?, ?, ?, ?, UNHEX(MD5(?)), CURRENT_TIMESTAMP() FROM DUAL WHERE + mdm_apple_configuration_profiles (profile_uuid, team_id, identifier, name, mobileconfig, checksum, uploaded_at, secrets_updated_at) +(SELECT ?, ?, ?, ?, ?, UNHEX(MD5(?)), CURRENT_TIMESTAMP(), ? FROM DUAL WHERE NOT EXISTS ( SELECT 1 FROM mdm_windows_configuration_profiles WHERE name = ? AND team_id = ? ) AND NOT EXISTS ( @@ -56,7 +56,8 @@ INSERT INTO var profileID int64 err := ds.withTx(ctx, func(tx sqlx.ExtContext) error { res, err := tx.ExecContext(ctx, stmt, - profUUID, teamID, cp.Identifier, cp.Name, cp.Mobileconfig, cp.Mobileconfig, cp.Name, teamID, cp.Name, teamID) + profUUID, teamID, cp.Identifier, cp.Name, cp.Mobileconfig, cp.Mobileconfig, cp.SecretsUpdatedAt, cp.Name, teamID, cp.Name, + teamID) if err != nil { switch { case IsDuplicate(err): @@ -213,7 +214,8 @@ SELECT mobileconfig, checksum, created_at, - uploaded_at + uploaded_at, + secrets_updated_at FROM mdm_apple_configuration_profiles WHERE @@ -277,8 +279,10 @@ SELECT identifier, raw_json, checksum, + token, created_at, - uploaded_at + uploaded_at, + secrets_updated_at FROM mdm_apple_declarations WHERE @@ -1701,7 +1705,8 @@ func (ds *Datastore) batchSetMDMAppleProfilesDB( SELECT identifier, profile_uuid, - mobileconfig + mobileconfig, + secrets_updated_at FROM mdm_apple_configuration_profiles WHERE @@ -1720,13 +1725,14 @@ WHERE const insertNewOrEditedProfile = ` INSERT INTO mdm_apple_configuration_profiles ( - profile_uuid, team_id, identifier, name, mobileconfig, checksum, uploaded_at + profile_uuid, team_id, identifier, name, mobileconfig, checksum, uploaded_at, secrets_updated_at ) VALUES -- see https://stackoverflow.com/a/51393124/1094941 - ( CONCAT('a', CONVERT(uuid() USING utf8mb4)), ?, ?, ?, ?, UNHEX(MD5(mobileconfig)), CURRENT_TIMESTAMP() ) + ( CONCAT('a', CONVERT(uuid() USING utf8mb4)), ?, ?, ?, ?, UNHEX(MD5(mobileconfig)), CURRENT_TIMESTAMP(6), ?) ON DUPLICATE KEY UPDATE - uploaded_at = IF(checksum = VALUES(checksum) AND name = VALUES(name), uploaded_at, CURRENT_TIMESTAMP()), + uploaded_at = IF(checksum = VALUES(checksum) AND name = VALUES(name), uploaded_at, CURRENT_TIMESTAMP(6)), + secrets_updated_at = VALUES(secrets_updated_at), checksum = VALUES(checksum), name = VALUES(name), mobileconfig = VALUES(mobileconfig) @@ -1809,7 +1815,7 @@ ON DUPLICATE KEY UPDATE // insert the new profiles and the ones that have changed for _, p := range incomingProfs { if result, err = tx.ExecContext(ctx, insertNewOrEditedProfile, profTeamID, p.Identifier, p.Name, - p.Mobileconfig); err != nil || strings.HasPrefix(ds.testBatchSetMDMAppleProfilesErr, "insert") { + p.Mobileconfig, p.SecretsUpdatedAt); err != nil || strings.HasPrefix(ds.testBatchSetMDMAppleProfilesErr, "insert") { if err == nil { err = errors.New(ds.testBatchSetMDMAppleProfilesErr) } @@ -1988,13 +1994,14 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB( ds.host_platform as host_platform, ds.profile_identifier as profile_identifier, ds.profile_name as profile_name, - ds.checksum as checksum + ds.checksum as checksum, + ds.secrets_updated_at as secrets_updated_at FROM ( %s ) as ds LEFT JOIN host_mdm_apple_profiles hmap ON hmap.profile_uuid = ds.profile_uuid AND hmap.host_uuid = ds.host_uuid WHERE - -- profile has been updated - ( hmap.checksum != ds.checksum ) OR + -- profile or secret variables have been updated + ( hmap.checksum != ds.checksum ) OR IFNULL(hmap.secrets_updated_at < ds.secrets_updated_at, FALSE) OR -- profiles in A but not in B ( hmap.profile_uuid IS NULL AND hmap.host_uuid IS NULL ) OR -- profiles in A and B but with operation type "remove" @@ -2060,6 +2067,7 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB( narrowByProfiles = "AND hmap.profile_uuid IN (?)" } + // Note: We do not need secrets_updated_at in the remove statement toRemoveStmt := fmt.Sprintf(` SELECT hmap.profile_uuid as profile_uuid, @@ -2176,46 +2184,7 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB( profilesToInsert := make(map[string]*fleet.MDMAppleProfilePayload) executeUpsertBatch := func(valuePart string, args []any) error { - // Check if the update needs to be done at all. - selectStmt := fmt.Sprintf(` - SELECT - host_uuid, - profile_uuid, - profile_identifier, - status, - COALESCE(operation_type, '') AS operation_type, - COALESCE(detail, '') AS detail, - command_uuid, - profile_name, - checksum, - profile_uuid - FROM host_mdm_apple_profiles WHERE (host_uuid, profile_uuid) IN (%s)`, - strings.TrimSuffix(strings.Repeat("(?,?),", len(profilesToInsert)), ",")) - var selectArgs []any - for _, p := range profilesToInsert { - selectArgs = append(selectArgs, p.HostUUID, p.ProfileUUID) - } - var existingProfiles []fleet.MDMAppleProfilePayload - if err := sqlx.SelectContext(ctx, tx, &existingProfiles, selectStmt, selectArgs...); err != nil { - return ctxerr.Wrap(ctx, err, "bulk set pending profile status select existing") - } - var updateNeeded bool - if len(existingProfiles) == len(profilesToInsert) { - for _, exist := range existingProfiles { - insert, ok := profilesToInsert[fmt.Sprintf("%s\n%s", exist.HostUUID, exist.ProfileUUID)] - if !ok || !exist.Equal(*insert) { - updateNeeded = true - break - } - } - } else { - updateNeeded = true - } - if !updateNeeded { - // All profiles are already in the database, no need to update. - return nil - } - + // If this call is made, we assume the update must be done -- a new profile was added or existing one modified. updatedDB = true baseStmt := fmt.Sprintf(` INSERT INTO host_mdm_apple_profiles ( @@ -2224,6 +2193,7 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB( profile_identifier, profile_name, checksum, + secrets_updated_at, operation_type, status, command_uuid, @@ -2235,6 +2205,7 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB( status = VALUES(status), command_uuid = VALUES(command_uuid), checksum = VALUES(checksum), + secrets_updated_at = VALUES(secrets_updated_at), detail = VALUES(detail) `, strings.TrimSuffix(valuePart, ",")) @@ -2271,14 +2242,15 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB( HostUUID: p.HostUUID, HostPlatform: p.HostPlatform, Checksum: p.Checksum, + SecretsUpdatedAt: p.SecretsUpdatedAt, Status: pp.Status, OperationType: pp.OperationType, Detail: pp.Detail, CommandUUID: pp.CommandUUID, } - pargs = append(pargs, p.ProfileUUID, p.HostUUID, p.ProfileIdentifier, p.ProfileName, p.Checksum, + pargs = append(pargs, p.ProfileUUID, p.HostUUID, p.ProfileIdentifier, p.ProfileName, p.Checksum, p.SecretsUpdatedAt, pp.OperationType, pp.Status, pp.CommandUUID, pp.Detail) - psb.WriteString("(?, ?, ?, ?, ?, ?, ?, ?, ?),") + psb.WriteString("(?, ?, ?, ?, ?, ?, ?, ?, ?, ?),") batchCount++ if batchCount >= batchSize { @@ -2298,14 +2270,15 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB( HostUUID: p.HostUUID, HostPlatform: p.HostPlatform, Checksum: p.Checksum, + SecretsUpdatedAt: p.SecretsUpdatedAt, OperationType: fleet.MDMOperationTypeInstall, Status: nil, CommandUUID: "", Detail: "", } - pargs = append(pargs, p.ProfileUUID, p.HostUUID, p.ProfileIdentifier, p.ProfileName, p.Checksum, + pargs = append(pargs, p.ProfileUUID, p.HostUUID, p.ProfileIdentifier, p.ProfileName, p.Checksum, p.SecretsUpdatedAt, fleet.MDMOperationTypeInstall, nil, "", "") - psb.WriteString("(?, ?, ?, ?, ?, ?, ?, ?, ?),") + psb.WriteString("(?, ?, ?, ?, ?, ?, ?, ?, ?, ?),") batchCount++ if batchCount >= batchSize { @@ -2334,14 +2307,15 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB( HostUUID: p.HostUUID, HostPlatform: p.HostPlatform, Checksum: p.Checksum, + SecretsUpdatedAt: p.SecretsUpdatedAt, OperationType: fleet.MDMOperationTypeRemove, Status: nil, CommandUUID: "", Detail: "", } - pargs = append(pargs, p.ProfileUUID, p.HostUUID, p.ProfileIdentifier, p.ProfileName, p.Checksum, + pargs = append(pargs, p.ProfileUUID, p.HostUUID, p.ProfileIdentifier, p.ProfileName, p.Checksum, p.SecretsUpdatedAt, fleet.MDMOperationTypeRemove, nil, "", "") - psb.WriteString("(?, ?, ?, ?, ?, ?, ?, ?, ?),") + psb.WriteString("(?, ?, ?, ?, ?, ?, ?, ?, ?, ?),") batchCount++ if batchCount >= batchSize { @@ -2404,6 +2378,7 @@ func generateDesiredStateQuery(entityType string) string { mae.identifier as ${entityIdentifierColumn}, mae.name as ${entityNameColumn}, mae.checksum as checksum, + mae.secrets_updated_at as secrets_updated_at, 0 as ${countEntityLabelsColumn}, 0 as count_non_broken_labels, 0 as count_host_labels, @@ -2437,6 +2412,7 @@ func generateDesiredStateQuery(entityType string) string { mae.identifier as ${entityIdentifierColumn}, mae.name as ${entityNameColumn}, mae.checksum as checksum, + mae.secrets_updated_at as secrets_updated_at, COUNT(*) as ${countEntityLabelsColumn}, COUNT(mel.label_id) as count_non_broken_labels, COUNT(lm.label_id) as count_host_labels, @@ -2457,7 +2433,7 @@ func generateDesiredStateQuery(entityType string) string { ne.type = 'Device' AND ( %s ) GROUP BY - mae.${entityUUIDColumn}, h.uuid, h.platform, mae.identifier, mae.name, mae.checksum + mae.${entityUUIDColumn}, h.uuid, h.platform, mae.identifier, mae.name, mae.checksum, mae.secrets_updated_at HAVING ${countEntityLabelsColumn} > 0 AND count_host_labels = ${countEntityLabelsColumn} @@ -2475,6 +2451,7 @@ func generateDesiredStateQuery(entityType string) string { mae.identifier as ${entityIdentifierColumn}, mae.name as ${entityNameColumn}, mae.checksum as checksum, + mae.secrets_updated_at as secrets_updated_at, COUNT(*) as ${countEntityLabelsColumn}, COUNT(mel.label_id) as count_non_broken_labels, COUNT(lm.label_id) as count_host_labels, @@ -2499,7 +2476,7 @@ func generateDesiredStateQuery(entityType string) string { ne.type = 'Device' AND ( %s ) GROUP BY - mae.${entityUUIDColumn}, h.uuid, h.platform, mae.identifier, mae.name, mae.checksum + mae.${entityUUIDColumn}, h.uuid, h.platform, mae.identifier, mae.name, mae.checksum, mae.secrets_updated_at HAVING -- considers only the profiles with labels, without any broken label, with results reported after all labels were created and with the host not in any label ${countEntityLabelsColumn} > 0 AND ${countEntityLabelsColumn} = count_non_broken_labels AND ${countEntityLabelsColumn} = count_host_updated_after_labels AND count_host_labels = 0 @@ -2516,6 +2493,7 @@ func generateDesiredStateQuery(entityType string) string { mae.identifier as ${entityIdentifierColumn}, mae.name as ${entityNameColumn}, mae.checksum as checksum, + mae.secrets_updated_at as secrets_updated_at, COUNT(*) as ${countEntityLabelsColumn}, COUNT(mel.label_id) as count_non_broken_labels, COUNT(lm.label_id) as count_host_labels, @@ -2536,7 +2514,7 @@ func generateDesiredStateQuery(entityType string) string { ne.type = 'Device' AND ( %s ) GROUP BY - mae.${entityUUIDColumn}, h.uuid, h.platform, mae.identifier, mae.name, mae.checksum + mae.${entityUUIDColumn}, h.uuid, h.platform, mae.identifier, mae.name, mae.checksum, mae.secrets_updated_at HAVING ${countEntityLabelsColumn} > 0 AND count_host_labels >= 1 `, func(s string) string { return dynamicNames[s] }) @@ -2590,7 +2568,7 @@ func generateEntitiesToInstallQuery(entityType string) string { ON hmae.${entityUUIDColumn} = ds.${entityUUIDColumn} AND hmae.host_uuid = ds.host_uuid WHERE -- entity has been updated - ( hmae.checksum != ds.checksum ) OR + ( hmae.checksum != ds.checksum ) OR IFNULL(hmae.secrets_updated_at < ds.secrets_updated_at, FALSE) OR -- entity in A but not in B ( hmae.${entityUUIDColumn} IS NULL AND hmae.host_uuid IS NULL ) OR -- entities in A and B but with operation type "remove" @@ -2661,7 +2639,8 @@ func (ds *Datastore) ListMDMAppleProfilesToInstall(ctx context.Context) ([]*flee ds.host_platform, ds.profile_identifier, ds.profile_name, - ds.checksum + ds.checksum, + ds.secrets_updated_at FROM %s `, generateEntitiesToInstallQuery("profile")) var profiles []*fleet.MDMAppleProfilePayload @@ -2670,6 +2649,8 @@ func (ds *Datastore) ListMDMAppleProfilesToInstall(ctx context.Context) ([]*flee } func (ds *Datastore) ListMDMAppleProfilesToRemove(ctx context.Context) ([]*fleet.MDMAppleProfilePayload, error) { + // Note: although some of these values (like secrets_updated_at) are not strictly necessary for profile removal, + // we are keeping them here for consistency. query := fmt.Sprintf(` SELECT hmae.profile_uuid, @@ -2677,6 +2658,7 @@ func (ds *Datastore) ListMDMAppleProfilesToRemove(ctx context.Context) ([]*fleet hmae.profile_name, hmae.host_uuid, hmae.checksum, + hmae.secrets_updated_at, hmae.operation_type, COALESCE(hmae.detail, '') as detail, hmae.status, @@ -2718,6 +2700,7 @@ func (ds *Datastore) GetMDMAppleProfilesContents(ctx context.Context, uuids []st return results, nil } +// BulkUpsertMDMAppleHostProfiles is used to update the status of profile delivery to hosts. func (ds *Datastore) BulkUpsertMDMAppleHostProfiles(ctx context.Context, payload []*fleet.MDMAppleBulkUpsertHostProfilePayload) error { if len(payload) == 0 { return nil @@ -2734,7 +2717,8 @@ func (ds *Datastore) BulkUpsertMDMAppleHostProfiles(ctx context.Context, payload operation_type, detail, command_uuid, - checksum + checksum, + secrets_updated_at ) VALUES %s ON DUPLICATE KEY UPDATE @@ -2742,6 +2726,7 @@ func (ds *Datastore) BulkUpsertMDMAppleHostProfiles(ctx context.Context, payload operation_type = VALUES(operation_type), detail = VALUES(detail), checksum = VALUES(checksum), + secrets_updated_at = VALUES(secrets_updated_at), profile_identifier = VALUES(profile_identifier), profile_name = VALUES(profile_name), command_uuid = VALUES(command_uuid)`, @@ -2761,12 +2746,13 @@ func (ds *Datastore) BulkUpsertMDMAppleHostProfiles(ctx context.Context, payload } generateValueArgs := func(p *fleet.MDMAppleBulkUpsertHostProfilePayload) (string, []any) { - valuePart := "(?, ?, ?, ?, ?, ?, ?, ?, ?)," - args := []any{p.ProfileUUID, p.ProfileIdentifier, p.ProfileName, p.HostUUID, p.Status, p.OperationType, p.Detail, p.CommandUUID, p.Checksum} + valuePart := "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)," + args := []any{p.ProfileUUID, p.ProfileIdentifier, p.ProfileName, p.HostUUID, p.Status, p.OperationType, p.Detail, p.CommandUUID, + p.Checksum, p.SecretsUpdatedAt} return valuePart, args } - const defaultBatchSize = 1000 // results in this times 9 placeholders + const defaultBatchSize = 1000 // number of parameters is this times number of placeholders batchSize := defaultBatchSize if ds.testUpsertMDMDesiredProfilesBatchSize > 0 { batchSize = ds.testUpsertMDMDesiredProfilesBatchSize @@ -3221,19 +3207,20 @@ func (ds *Datastore) BulkUpsertMDMAppleConfigProfiles(ctx context.Context, paylo teamID = *cp.TeamID } - args = append(args, teamID, cp.Identifier, cp.Name, cp.Mobileconfig) + args = append(args, teamID, cp.Identifier, cp.Name, cp.Mobileconfig, cp.SecretsUpdatedAt) // see https://stackoverflow.com/a/51393124/1094941 - sb.WriteString("( CONCAT('a', CONVERT(uuid() USING utf8mb4)), ?, ?, ?, ?, UNHEX(MD5(mobileconfig)), CURRENT_TIMESTAMP() ),") + sb.WriteString("( CONCAT('a', CONVERT(uuid() USING utf8mb4)), ?, ?, ?, ?, UNHEX(MD5(mobileconfig)), CURRENT_TIMESTAMP(), ?),") } stmt := fmt.Sprintf(` INSERT INTO - mdm_apple_configuration_profiles (profile_uuid, team_id, identifier, name, mobileconfig, checksum, uploaded_at) + mdm_apple_configuration_profiles (profile_uuid, team_id, identifier, name, mobileconfig, checksum, uploaded_at, secrets_updated_at) VALUES %s ON DUPLICATE KEY UPDATE uploaded_at = IF(checksum = VALUES(checksum) AND name = VALUES(name), uploaded_at, CURRENT_TIMESTAMP()), mobileconfig = VALUES(mobileconfig), - checksum = VALUES(checksum) + checksum = VALUES(checksum), + secrets_updated_at = VALUES(secrets_updated_at) `, strings.TrimSuffix(sb.String(), ",")) if _, err := ds.writer(ctx).ExecContext(ctx, stmt, args...); err != nil { @@ -4230,15 +4217,17 @@ INSERT INTO mdm_apple_declarations ( name, raw_json, checksum, + secrets_updated_at, uploaded_at, team_id ) VALUES ( - ?,?,?,?,UNHEX(?),CURRENT_TIMESTAMP(),? + ?,?,?,?,UNHEX(MD5(raw_json)),?,CURRENT_TIMESTAMP(),? ) ON DUPLICATE KEY UPDATE uploaded_at = IF(checksum = VALUES(checksum) AND name = VALUES(name), uploaded_at, CURRENT_TIMESTAMP()), checksum = VALUES(checksum), + secrets_updated_at = VALUES(secrets_updated_at), name = VALUES(name), identifier = VALUES(identifier), raw_json = VALUES(raw_json) @@ -4338,14 +4327,13 @@ WHERE } for _, d := range incomingDeclarations { - checksum := md5ChecksumScriptContent(string(d.RawJSON)) declUUID := fleet.MDMAppleDeclarationUUIDPrefix + uuid.NewString() if result, err = tx.ExecContext(ctx, insertStmt, declUUID, d.Identifier, d.Name, d.RawJSON, - checksum, + d.SecretsUpdatedAt, declTeamID); err != nil || strings.HasPrefix(ds.testBatchSetMDMAppleProfilesErr, "insert") { if err == nil { err = errors.New(ds.testBatchSetMDMAppleProfilesErr) @@ -4421,8 +4409,9 @@ INSERT INTO mdm_apple_declarations ( name, raw_json, checksum, + secrets_updated_at, uploaded_at) -(SELECT ?,?,?,?,?,UNHEX(?),CURRENT_TIMESTAMP() FROM DUAL WHERE +(SELECT ?,?,?,?,?,UNHEX(MD5(?)),?,CURRENT_TIMESTAMP() FROM DUAL WHERE NOT EXISTS ( SELECT 1 FROM mdm_windows_configuration_profiles WHERE name = ? AND team_id = ? ) AND NOT EXISTS ( @@ -4442,8 +4431,9 @@ INSERT INTO mdm_apple_declarations ( name, raw_json, checksum, + secrets_updated_at, uploaded_at) -(SELECT ?,?,?,?,?,UNHEX(?),CURRENT_TIMESTAMP() FROM DUAL WHERE +(SELECT ?,?,?,?,?,UNHEX(MD5(?)),?,CURRENT_TIMESTAMP() FROM DUAL WHERE NOT EXISTS ( SELECT 1 FROM mdm_windows_configuration_profiles WHERE name = ? AND team_id = ? ) AND NOT EXISTS ( @@ -4461,7 +4451,6 @@ ON DUPLICATE KEY UPDATE func (ds *Datastore) insertOrUpsertMDMAppleDeclaration(ctx context.Context, insOrUpsertStmt string, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) { declUUID := fleet.MDMAppleDeclarationUUIDPrefix + uuid.NewString() - checksum := md5ChecksumScriptContent(string(declaration.RawJSON)) var tmID uint if declaration.TeamID != nil { @@ -4472,7 +4461,9 @@ func (ds *Datastore) insertOrUpsertMDMAppleDeclaration(ctx context.Context, insO err := ds.withTx(ctx, func(tx sqlx.ExtContext) error { res, err := tx.ExecContext(ctx, insOrUpsertStmt, - declUUID, tmID, declaration.Identifier, declaration.Name, declaration.RawJSON, checksum, declaration.Name, tmID, declaration.Name, tmID) + declUUID, tmID, declaration.Identifier, declaration.Name, declaration.RawJSON, declaration.RawJSON, + declaration.SecretsUpdatedAt, + declaration.Name, tmID, declaration.Name, tmID) if err != nil { switch { case IsDuplicate(err): @@ -4652,9 +4643,9 @@ func batchSetDeclarationLabelAssociationsDB(ctx context.Context, tx sqlx.ExtCont func (ds *Datastore) MDMAppleDDMDeclarationsToken(ctx context.Context, hostUUID string) (*fleet.MDMAppleDDMDeclarationsToken, error) { const stmt = ` SELECT - COALESCE(MD5((count(0) + GROUP_CONCAT(HEX(mad.checksum) + COALESCE(MD5((count(0) + GROUP_CONCAT(HEX(mad.token) ORDER BY - mad.uploaded_at DESC separator ''))), '') AS checksum, + mad.uploaded_at DESC separator ''))), '') AS token, COALESCE(MAX(mad.created_at), NOW()) AS latest_created_timestamp FROM host_mdm_apple_declarations hmad @@ -4681,7 +4672,7 @@ WHERE func (ds *Datastore) MDMAppleDDMDeclarationItems(ctx context.Context, hostUUID string) ([]fleet.MDMAppleDDMDeclarationItem, error) { const stmt = ` SELECT - HEX(mad.checksum) as checksum, + HEX(mad.token) as token, mad.identifier FROM host_mdm_apple_declarations hmad @@ -4704,7 +4695,7 @@ func (ds *Datastore) MDMAppleDDMDeclarationsResponse(ctx context.Context, identi // declarations are removed, but the join would provide an extra layer of safety. const stmt = ` SELECT - mad.raw_json, HEX(mad.checksum) as checksum + mad.raw_json, HEX(mad.token) as token FROM host_mdm_apple_declarations hmad JOIN mdm_apple_declarations mad ON hmad.declaration_uuid = mad.declaration_uuid @@ -4792,13 +4783,14 @@ func mdmAppleBatchSetPendingHostDeclarationsDB( ) (updatedDB bool, err error) { baseStmt := ` INSERT INTO host_mdm_apple_declarations - (host_uuid, status, operation_type, checksum, declaration_uuid, declaration_identifier, declaration_name) + (host_uuid, status, operation_type, checksum, secrets_updated_at, declaration_uuid, declaration_identifier, declaration_name) VALUES %s ON DUPLICATE KEY UPDATE status = VALUES(status), operation_type = VALUES(operation_type), - checksum = VALUES(checksum) + checksum = VALUES(checksum), + secrets_updated_at = VALUES(secrets_updated_at) ` profilesToInsert := make(map[string]*fleet.MDMAppleHostDeclaration) @@ -4813,6 +4805,7 @@ func mdmAppleBatchSetPendingHostDeclarationsDB( COALESCE(operation_type, '') AS operation_type, COALESCE(detail, '') AS detail, checksum, + secrets_updated_at, declaration_uuid, declaration_identifier, declaration_name @@ -4855,17 +4848,18 @@ func mdmAppleBatchSetPendingHostDeclarationsDB( generateValueArgs := func(d *fleet.MDMAppleHostDeclaration) (string, []any) { profilesToInsert[fmt.Sprintf("%s\n%s", d.HostUUID, d.DeclarationUUID)] = &fleet.MDMAppleHostDeclaration{ - HostUUID: d.HostUUID, - DeclarationUUID: d.DeclarationUUID, - Name: d.Name, - Identifier: d.Identifier, - Status: status, - OperationType: d.OperationType, - Detail: d.Detail, - Checksum: d.Checksum, - } - valuePart := "(?, ?, ?, ?, ?, ?, ?)," - args := []any{d.HostUUID, status, d.OperationType, d.Checksum, d.DeclarationUUID, d.Identifier, d.Name} + HostUUID: d.HostUUID, + DeclarationUUID: d.DeclarationUUID, + Name: d.Name, + Identifier: d.Identifier, + Status: status, + OperationType: d.OperationType, + Detail: d.Detail, + Checksum: d.Checksum, + SecretsUpdatedAt: d.SecretsUpdatedAt, + } + valuePart := "(?, ?, ?, ?, ?, ?, ?, ?)," + args := []any{d.HostUUID, status, d.OperationType, d.Checksum, d.SecretsUpdatedAt, d.DeclarationUUID, d.Identifier, d.Name} return valuePart, args } @@ -4875,9 +4869,14 @@ func mdmAppleBatchSetPendingHostDeclarationsDB( // mdmAppleGetHostsWithChangedDeclarationsDB returns a // MDMAppleHostDeclaration item for each (host x declaration) pair that -// needs an status change, this includes declarations to install and +// needs a status change, this includes declarations to install and // declarations to be removed. Those can be differentiated by the // OperationType field on each struct. +// +// Note (2024/12/24): This method returns some rows that DO NOT NEED TO BE UPDATED. +// We should optimize this method to only return the rows that need to be updated. +// Then we can eliminate the subsequent check for updates in the caller. +// The check for updates is needed to log the correct activity item -- whether declarations were updated or not. func mdmAppleGetHostsWithChangedDeclarationsDB(ctx context.Context, tx sqlx.ExtContext) ([]*fleet.MDMAppleHostDeclaration, error) { stmt := fmt.Sprintf(` ( @@ -4885,6 +4884,7 @@ func mdmAppleGetHostsWithChangedDeclarationsDB(ctx context.Context, tx sqlx.ExtC ds.host_uuid, 'install' as operation_type, ds.checksum, + ds.secrets_updated_at, ds.declaration_uuid, ds.declaration_identifier, ds.declaration_name @@ -4897,6 +4897,7 @@ func mdmAppleGetHostsWithChangedDeclarationsDB(ctx context.Context, tx sqlx.ExtC hmae.host_uuid, 'remove' as operation_type, hmae.checksum, + hmae.secrets_updated_at, hmae.declaration_uuid, hmae.declaration_identifier, hmae.declaration_name @@ -4915,16 +4916,17 @@ func mdmAppleGetHostsWithChangedDeclarationsDB(ctx context.Context, tx sqlx.ExtC return decls, nil } +// MDMAppleStoreDDMStatusReport updates the status of the host's declarations. 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 + SELECT host_uuid, status, operation_type, HEX(checksum) as checksum, secrets_updated_at, 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) + (host_uuid, declaration_uuid, status, operation_type, detail, declaration_name, declaration_identifier, checksum, secrets_updated_at) VALUES %s ON DUPLICATE KEY UPDATE @@ -4952,8 +4954,9 @@ ON DUPLICATE KEY UPDATE 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) + insertVals.WriteString("(?, ?, ?, ?, ?, ?, ?, UNHEX(?), ?),") + args = append(args, hostUUID, c.DeclarationUUID, u.Status, u.OperationType, u.Detail, c.Identifier, c.Name, c.Checksum, + c.SecretsUpdatedAt) } } diff --git a/server/datastore/mysql/apple_mdm_test.go b/server/datastore/mysql/apple_mdm_test.go index a975c908ac90..43b1e9df00e7 100644 --- a/server/datastore/mysql/apple_mdm_test.go +++ b/server/datastore/mysql/apple_mdm_test.go @@ -1056,6 +1056,7 @@ func expectAppleProfiles( gotp.ProfileUUID = "" gotp.CreatedAt = time.Time{} + gotp.SecretsUpdatedAt = nil // if an expected uploaded_at timestamp is provided for this profile, keep // its value, otherwise clear it as we don't care about asserting its @@ -1119,7 +1120,7 @@ func expectAppleDeclarations( require.NotEmpty(t, gotD.DeclarationUUID) require.True(t, strings.HasPrefix(gotD.DeclarationUUID, fleet.MDMAppleDeclarationUUIDPrefix)) gotD.DeclarationUUID = "" - gotD.Checksum = "" // don't care about md5checksum here + gotD.Token = "" // don't care about md5checksum here gotD.CreatedAt = time.Time{} @@ -1399,6 +1400,7 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) { for _, p := range got { require.NotEmpty(t, p.Checksum) p.Checksum = nil + p.SecretsUpdatedAt = nil } require.ElementsMatch(t, want, got) } @@ -7336,7 +7338,9 @@ func testMDMAppleProfileLabels(t *testing.T, ds *Datastore) { matchProfiles := func(want, got []*fleet.MDMAppleProfilePayload) { // match only the fields we care about for _, p := range got { + assert.NotEmpty(t, p.Checksum) p.Checksum = nil + p.SecretsUpdatedAt = nil } require.ElementsMatch(t, want, got) } diff --git a/server/datastore/mysql/mdm.go b/server/datastore/mysql/mdm.go index 5943b7ba8a4e..687de8d5978a 100644 --- a/server/datastore/mysql/mdm.go +++ b/server/datastore/mysql/mdm.go @@ -543,6 +543,10 @@ OR // (and my hunch is that we could even do the same for // profiles) but this could be optimized to use only a provided // set of host uuids. + // + // Note(victor): Why is the status being set to nil? Shouldn't it be set to pending? + // Or at least pending for install and nil for remove profiles. Please update this comment if you know. + // This method is called bulkSetPendingMDMHostProfilesDB, so it is confusing that the status is NOT explicitly set to pending. _, updates.AppleDeclaration, err = mdmAppleBatchSetHostDeclarationStateDB(ctx, tx, batchSize, nil) if err != nil { return updates, ctxerr.Wrap(ctx, err, "bulk set pending apple declarations") diff --git a/server/datastore/mysql/microsoft_mdm.go b/server/datastore/mysql/microsoft_mdm.go index 360a975a3c76..60fe562552ba 100644 --- a/server/datastore/mysql/microsoft_mdm.go +++ b/server/datastore/mysql/microsoft_mdm.go @@ -1777,6 +1777,7 @@ WHERE team_id = ? ` + // For Windows profiles, if team_id and name are the same, we do an update. Otherwise, we do an insert. const insertNewOrEditedProfile = ` INSERT INTO mdm_windows_configuration_profiles ( diff --git a/server/datastore/mysql/migrations/tables/20241230000000_AddSecretsUpdatedAt.go b/server/datastore/mysql/migrations/tables/20241230000000_AddSecretsUpdatedAt.go new file mode 100644 index 000000000000..9acffb9e184d --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20241230000000_AddSecretsUpdatedAt.go @@ -0,0 +1,54 @@ +package tables + +import ( + "database/sql" + "fmt" +) + +func init() { + MigrationClient.AddMigration(Up_20241230000000, Down_20241230000000) +} + +func Up_20241230000000(tx *sql.Tx) error { + // Using DATETIME instead of TIMESTAMP for secrets_updated_at to avoid future Y2K38 issues, + // since this date is used to detect if profile needs to be updated. + + // secrets_updated_at are updated when profile contents have not changed but secret variables in the profile have changed + _, err := tx.Exec(`ALTER TABLE mdm_apple_configuration_profiles + ADD COLUMN secrets_updated_at DATETIME(6) NULL, + MODIFY COLUMN created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + MODIFY COLUMN uploaded_at TIMESTAMP(6) NULL DEFAULT NULL`) + if err != nil { + return fmt.Errorf("failed to alter mdm_apple_configuration_profiles table: %w", err) + } + + _, err = tx.Exec(`ALTER TABLE host_mdm_apple_profiles + ADD COLUMN secrets_updated_at DATETIME(6) NULL`) + if err != nil { + return fmt.Errorf("failed to add secrets_updated_at to host_mdm_apple_profiles table: %w", err) + } + + // secrets_updated_at are updated when profile contents have not changed but secret variables in the profile have changed + _, err = tx.Exec(`ALTER TABLE mdm_apple_declarations + ADD COLUMN secrets_updated_at DATETIME(6) NULL, + MODIFY COLUMN created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + MODIFY COLUMN uploaded_at TIMESTAMP(6) NULL DEFAULT NULL, + -- token is used to identify if declaration needs to be re-applied + ADD COLUMN token BINARY(16) GENERATED ALWAYS AS + (UNHEX(MD5(CONCAT(raw_json, IFNULL(secrets_updated_at, ''))))) STORED NULL`) + if err != nil { + return fmt.Errorf("failed to alter mdm_apple_declarations table: %w", err) + } + + _, err = tx.Exec(`ALTER TABLE host_mdm_apple_declarations + ADD COLUMN secrets_updated_at DATETIME(6) NULL`) + if err != nil { + return fmt.Errorf("failed to alter host_mdm_apple_declarations table: %w", err) + } + + return nil +} + +func Down_20241230000000(_ *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 46ffe5f74358..0dff207929d8 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -423,12 +423,13 @@ CREATE TABLE `host_mdm_apple_declarations` ( `declaration_uuid` varchar(37) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', `declaration_identifier` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `declaration_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `secrets_updated_at` datetime(6) DEFAULT NULL, PRIMARY KEY (`host_uuid`,`declaration_uuid`), KEY `status` (`status`), KEY `operation_type` (`operation_type`), CONSTRAINT `host_mdm_apple_declarations_ibfk_1` FOREIGN KEY (`status`) REFERENCES `mdm_delivery_status` (`status`) ON UPDATE CASCADE, CONSTRAINT `host_mdm_apple_declarations_ibfk_2` FOREIGN KEY (`operation_type`) REFERENCES `mdm_operation_types` (`operation_type`) ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; @@ -445,6 +446,7 @@ CREATE TABLE `host_mdm_apple_profiles` ( `profile_uuid` varchar(37) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', `created_at` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updated_at` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + `secrets_updated_at` datetime(6) DEFAULT NULL, PRIMARY KEY (`host_uuid`,`profile_uuid`), KEY `status` (`status`), KEY `operation_type` (`operation_type`), @@ -849,10 +851,11 @@ CREATE TABLE `mdm_apple_configuration_profiles` ( `identifier` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, `mobileconfig` mediumblob NOT NULL, - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - `uploaded_at` timestamp NULL DEFAULT NULL, + `created_at` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `uploaded_at` timestamp(6) NULL DEFAULT NULL, `checksum` binary(16) NOT NULL, `profile_uuid` varchar(37) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `secrets_updated_at` datetime(6) DEFAULT NULL, PRIMARY KEY (`profile_uuid`), UNIQUE KEY `idx_mdm_apple_config_prof_team_identifier` (`team_id`,`identifier`), UNIQUE KEY `idx_mdm_apple_config_prof_team_name` (`team_id`,`name`), @@ -879,9 +882,11 @@ CREATE TABLE `mdm_apple_declarations` ( `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `raw_json` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `checksum` binary(16) NOT NULL, - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - `uploaded_at` timestamp NULL DEFAULT NULL, + `created_at` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `uploaded_at` timestamp(6) NULL DEFAULT NULL, `auto_increment` bigint NOT NULL AUTO_INCREMENT, + `secrets_updated_at` datetime(6) DEFAULT NULL, + `token` binary(16) GENERATED ALWAYS AS (unhex(md5(concat(`raw_json`,ifnull(`secrets_updated_at`,_utf8mb4''))))) STORED, PRIMARY KEY (`declaration_uuid`), UNIQUE KEY `idx_mdm_apple_declaration_team_identifier` (`team_id`,`identifier`), UNIQUE KEY `idx_mdm_apple_declaration_team_name` (`team_id`,`name`), @@ -1106,9 +1111,9 @@ CREATE TABLE `migration_status_tables` ( `is_applied` tinyint(1) NOT NULL, `tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) -) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=343 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=344 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241110152839,1,'2020-01-01 01:01:01'),(328,20241110152840,1,'2020-01-01 01:01:01'),(329,20241110152841,1,'2020-01-01 01:01:01'),(330,20241116233322,1,'2020-01-01 01:01:01'),(331,20241122171434,1,'2020-01-01 01:01:01'),(332,20241125150614,1,'2020-01-01 01:01:01'),(333,20241203125346,1,'2020-01-01 01:01:01'),(334,20241203130032,1,'2020-01-01 01:01:01'),(335,20241205122800,1,'2020-01-01 01:01:01'),(336,20241209164540,1,'2020-01-01 01:01:01'),(337,20241210140021,1,'2020-01-01 01:01:01'),(338,20241219180042,1,'2020-01-01 01:01:01'),(339,20241220100000,1,'2020-01-01 01:01:01'),(340,20241220114903,1,'2020-01-01 01:01:01'),(341,20241220114904,1,'2020-01-01 01:01:01'),(342,20241224000000,1,'2020-01-01 01:01:01'); +INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241110152839,1,'2020-01-01 01:01:01'),(328,20241110152840,1,'2020-01-01 01:01:01'),(329,20241110152841,1,'2020-01-01 01:01:01'),(330,20241116233322,1,'2020-01-01 01:01:01'),(331,20241122171434,1,'2020-01-01 01:01:01'),(332,20241125150614,1,'2020-01-01 01:01:01'),(333,20241203125346,1,'2020-01-01 01:01:01'),(334,20241203130032,1,'2020-01-01 01:01:01'),(335,20241205122800,1,'2020-01-01 01:01:01'),(336,20241209164540,1,'2020-01-01 01:01:01'),(337,20241210140021,1,'2020-01-01 01:01:01'),(338,20241219180042,1,'2020-01-01 01:01:01'),(339,20241220100000,1,'2020-01-01 01:01:01'),(340,20241220114903,1,'2020-01-01 01:01:01'),(341,20241220114904,1,'2020-01-01 01:01:01'),(342,20241224000000,1,'2020-01-01 01:01:01'),(343,20241230000000,1,'2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `mobile_device_management_solutions` ( diff --git a/server/datastore/mysql/secret_variables.go b/server/datastore/mysql/secret_variables.go index 19245dbc76f8..14aa17cab7af 100644 --- a/server/datastore/mysql/secret_variables.go +++ b/server/datastore/mysql/secret_variables.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strings" + "time" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" @@ -15,24 +16,69 @@ func (ds *Datastore) UpsertSecretVariables(ctx context.Context, secretVariables return nil } - values := strings.TrimSuffix(strings.Repeat("(?,?),", len(secretVariables)), ",") + // The secret variables should rarely change, so we do not use a transaction here. + // When we encrypt a secret variable, it is salted, so the encrypted data is different each time. + // In order to keep the updated_at timestamp correct, we need to compare the encrypted value + // with the existing value in the database. If the values are the same, we do not update the row. - stmt := fmt.Sprintf(` - INSERT INTO secret_variables (name, value) - VALUES %s - ON DUPLICATE KEY UPDATE value = VALUES(value)`, values) - - args := make([]interface{}, 0, len(secretVariables)*2) + var names []string for _, secretVariable := range secretVariables { - valueEncrypted, err := encrypt([]byte(secretVariable.Value), ds.serverPrivateKey) - if err != nil { - return ctxerr.Wrap(ctx, err, "encrypt secret value with server private key") + names = append(names, secretVariable.Name) + } + existingVariables, err := ds.GetSecretVariables(ctx, names) + if err != nil { + return ctxerr.Wrap(ctx, err, "get existing secret variables") + } + existingVariableMap := make(map[string]string, len(existingVariables)) + for _, existingVariable := range existingVariables { + existingVariableMap[existingVariable.Name] = existingVariable.Value + } + var variablesToInsert []fleet.SecretVariable + var variablesToUpdate []fleet.SecretVariable + for _, secretVariable := range secretVariables { + existingValue, ok := existingVariableMap[secretVariable.Name] + switch { + case !ok: + variablesToInsert = append(variablesToInsert, secretVariable) + case existingValue != secretVariable.Value: + variablesToUpdate = append(variablesToUpdate, secretVariable) + default: + // No change -- the variable value is the same + } + } + + if len(variablesToInsert) > 0 { + values := strings.TrimSuffix(strings.Repeat("(?,?),", len(variablesToInsert)), ",") + stmt := fmt.Sprintf(` + INSERT INTO secret_variables (name, value) + VALUES %s`, values) + args := make([]interface{}, 0, len(variablesToInsert)*2) + for _, secretVariable := range variablesToInsert { + valueEncrypted, err := encrypt([]byte(secretVariable.Value), ds.serverPrivateKey) + if err != nil { + return ctxerr.Wrap(ctx, err, "encrypt secret value for insert with server private key") + } + args = append(args, secretVariable.Name, valueEncrypted) + } + if _, err := ds.writer(ctx).ExecContext(ctx, stmt, args...); err != nil { + return ctxerr.Wrap(ctx, err, "insert secret variables") } - args = append(args, secretVariable.Name, valueEncrypted) } - if _, err := ds.writer(ctx).ExecContext(ctx, stmt, args...); err != nil { - return ctxerr.Wrap(ctx, err, "upsert secret variables") + if len(variablesToUpdate) > 0 { + stmt := ` + UPDATE secret_variables + SET value = ? + WHERE name = ?` + for _, secretVariable := range variablesToUpdate { + valueEncrypted, err := encrypt([]byte(secretVariable.Value), ds.serverPrivateKey) + if err != nil { + return ctxerr.Wrap(ctx, err, "encrypt secret value for update with server private key") + } + if _, err := ds.writer(ctx).ExecContext(ctx, stmt, valueEncrypted, secretVariable.Name); err != nil { + return ctxerr.Wrap(ctx, err, "update secret variables") + } + } } return nil @@ -44,7 +90,7 @@ func (ds *Datastore) GetSecretVariables(ctx context.Context, names []string) ([] } stmt, args, err := sqlx.In(` - SELECT name, value + SELECT name, value, updated_at FROM secret_variables WHERE name IN (?)`, names) if err != nil { @@ -70,14 +116,19 @@ func (ds *Datastore) GetSecretVariables(ctx context.Context, names []string) ([] } func (ds *Datastore) ExpandEmbeddedSecrets(ctx context.Context, document string) (string, error) { + expanded, _, err := ds.expandEmbeddedSecrets(ctx, document) + return expanded, err +} + +func (ds *Datastore) expandEmbeddedSecrets(ctx context.Context, document string) (string, []fleet.SecretVariable, error) { embeddedSecrets := fleet.ContainsPrefixVars(document, fleet.ServerSecretPrefix) if len(embeddedSecrets) == 0 { - return document, nil + return document, nil, nil } secrets, err := ds.GetSecretVariables(ctx, embeddedSecrets) if err != nil { - return "", ctxerr.Wrap(ctx, err, "expanding embedded secrets") + return "", nil, ctxerr.Wrap(ctx, err, "expanding embedded secrets") } secretMap := make(map[string]string, len(secrets)) @@ -95,7 +146,7 @@ func (ds *Datastore) ExpandEmbeddedSecrets(ctx context.Context, document string) } if len(missingSecrets) > 0 { - return "", fleet.MissingSecretsError{MissingSecrets: missingSecrets} + return "", nil, fleet.MissingSecretsError{MissingSecrets: missingSecrets} } expanded := fleet.MaybeExpand(document, func(s string) (string, bool) { @@ -106,7 +157,25 @@ func (ds *Datastore) ExpandEmbeddedSecrets(ctx context.Context, document string) return val, ok }) - return expanded, nil + return expanded, secrets, nil +} + +func (ds *Datastore) ExpandEmbeddedSecretsAndUpdatedAt(ctx context.Context, document string) (string, *time.Time, error) { + expanded, secrets, err := ds.expandEmbeddedSecrets(ctx, document) + if err != nil { + return "", nil, ctxerr.Wrap(ctx, err, "expanding embedded secrets and updated at") + } + if len(secrets) == 0 { + return expanded, nil, nil + } + // Find the most recent updated_at timestamp + var updatedAt time.Time + for _, secret := range secrets { + if secret.UpdatedAt.After(updatedAt) { + updatedAt = secret.UpdatedAt + } + } + return expanded, &updatedAt, err } func (ds *Datastore) ValidateEmbeddedSecrets(ctx context.Context, documents []string) error { diff --git a/server/datastore/mysql/secret_variables_test.go b/server/datastore/mysql/secret_variables_test.go index 8936442b58f0..a4ad33e1db69 100644 --- a/server/datastore/mysql/secret_variables_test.go +++ b/server/datastore/mysql/secret_variables_test.go @@ -59,18 +59,33 @@ func testUpsertSecretVariables(t *testing.T, ds *Datastore) { assert.Equal(t, secretMap[result.Name], result.Value) } - // Update a secret + // Update a secret and insert a new one secretMap["test2"] = "newTestValue2" + secretMap["test4"] = "testValue4" err = ds.UpsertSecretVariables(ctx, []fleet.SecretVariable{ {Name: "test2", Value: secretMap["test2"]}, + {Name: "test4", Value: secretMap["test4"]}, }) assert.NoError(t, err) - results, err = ds.GetSecretVariables(ctx, []string{"test2"}) + results, err = ds.GetSecretVariables(ctx, []string{"test2", "test4"}) assert.NoError(t, err) - require.Len(t, results, 1) - assert.Equal(t, "test2", results[0].Name) - assert.Equal(t, secretMap[results[0].Name], results[0].Value) + require.Len(t, results, 2) + for _, result := range results { + assert.Equal(t, secretMap[result.Name], result.Value) + } + // Make sure updated_at timestamp does not change when we update a secret with the same value + original, err := ds.GetSecretVariables(ctx, []string{"test1"}) + require.NoError(t, err) + require.Len(t, original, 1) + err = ds.UpsertSecretVariables(ctx, []fleet.SecretVariable{ + {Name: "test1", Value: secretMap["test1"]}, + }) + require.NoError(t, err) + updated, err := ds.GetSecretVariables(ctx, []string{"test1"}) + require.NoError(t, err) + require.Len(t, original, 1) + assert.Equal(t, original[0], updated[0]) } func testValidateEmbeddedSecrets(t *testing.T, ds *Datastore) { @@ -122,7 +137,7 @@ Hello doc${FLEET_SECRET_INVALID}. $FLEET_SECRET_ALSO_INVALID func testExpandEmbeddedSecrets(t *testing.T, ds *Datastore) { noSecrets := ` -This document contains to fleet secrets. +This document contains no fleet secrets. $FLEET_VAR_XX $HOSTNAME ${SOMETHING_ELSE} ` @@ -157,10 +172,18 @@ Hello doc${FLEET_SECRET_INVALID}. $FLEET_SECRET_ALSO_INVALID expanded, err := ds.ExpandEmbeddedSecrets(ctx, noSecrets) require.NoError(t, err) require.Equal(t, noSecrets, expanded) + expanded, secretsUpdatedAt, err := ds.ExpandEmbeddedSecretsAndUpdatedAt(ctx, noSecrets) + require.NoError(t, err) + require.Equal(t, noSecrets, expanded) + assert.Nil(t, secretsUpdatedAt) expanded, err = ds.ExpandEmbeddedSecrets(ctx, validSecret) require.NoError(t, err) require.Equal(t, validSecretExpanded, expanded) + expanded, secretsUpdatedAt, err = ds.ExpandEmbeddedSecretsAndUpdatedAt(ctx, validSecret) + require.NoError(t, err) + require.Equal(t, validSecretExpanded, expanded) + assert.NotNil(t, secretsUpdatedAt) _, err = ds.ExpandEmbeddedSecrets(ctx, invalidSecret) require.ErrorContains(t, err, "$FLEET_SECRET_INVALID") diff --git a/server/fleet/apple_mdm.go b/server/fleet/apple_mdm.go index 3cf0378e9114..7884f1300350 100644 --- a/server/fleet/apple_mdm.go +++ b/server/fleet/apple_mdm.go @@ -1,7 +1,6 @@ package fleet import ( - "bytes" "context" "crypto/md5" // nolint: gosec "encoding/hex" @@ -205,6 +204,7 @@ type MDMAppleConfigProfile struct { LabelsExcludeAny []ConfigurationProfileLabel `db:"-" json:"labels_exclude_any,omitempty"` CreatedAt time.Time `db:"created_at" json:"created_at"` UploadedAt time.Time `db:"uploaded_at" json:"updated_at"` // NOTE: JSON field is still `updated_at` for historical reasons, would be an API breaking change + SecretsUpdatedAt *time.Time `db:"secrets_updated_at" json:"-"` } // MDMProfilesUpdates flags updates that were done during batch processing of profiles. @@ -321,6 +321,7 @@ type MDMAppleProfilePayload struct { HostUUID string `db:"host_uuid"` HostPlatform string `db:"host_platform"` Checksum []byte `db:"checksum"` + SecretsUpdatedAt *time.Time `db:"secrets_updated_at"` Status *MDMDeliveryStatus `db:"status" json:"status"` OperationType MDMOperationType `db:"operation_type"` Detail string `db:"detail"` @@ -333,20 +334,6 @@ func (p *MDMAppleProfilePayload) DidNotInstallOnHost() bool { return p.Status != nil && (*p.Status == MDMDeliveryFailed || *p.Status == MDMDeliveryPending) && p.OperationType == MDMOperationTypeInstall } -func (p MDMAppleProfilePayload) Equal(other MDMAppleProfilePayload) bool { - statusEqual := p.Status == nil && other.Status == nil || p.Status != nil && other.Status != nil && *p.Status == *other.Status - return p.ProfileUUID == other.ProfileUUID && - p.ProfileIdentifier == other.ProfileIdentifier && - p.ProfileName == other.ProfileName && - p.HostUUID == other.HostUUID && - p.HostPlatform == other.HostPlatform && - bytes.Equal(p.Checksum, other.Checksum) && - statusEqual && - p.OperationType == other.OperationType && - p.Detail == other.Detail && - p.CommandUUID == other.CommandUUID -} - type MDMAppleBulkUpsertHostProfilePayload struct { ProfileUUID string ProfileIdentifier string @@ -357,6 +344,7 @@ type MDMAppleBulkUpsertHostProfilePayload struct { Status *MDMDeliveryStatus Detail string Checksum []byte + SecretsUpdatedAt *time.Time } // MDMAppleFileVaultSummary reports the number of macOS hosts being managed with Apples disk @@ -603,13 +591,18 @@ type MDMAppleDeclaration struct { // Checksum is a checksum of the JSON contents Checksum string `db:"checksum" json:"-"` + // Token is used to identify if declaration needs to be re-applied. + // It contains the checksum of the JSON contents and secrets updated timestamp (if secret variables are present). + Token string `db:"token" json:"-"` + // labels associated with this Declaration LabelsIncludeAll []ConfigurationProfileLabel `db:"-" json:"labels_include_all,omitempty"` LabelsIncludeAny []ConfigurationProfileLabel `db:"-" json:"labels_include_any,omitempty"` LabelsExcludeAny []ConfigurationProfileLabel `db:"-" json:"labels_exclude_any,omitempty"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UploadedAt time.Time `db:"uploaded_at" json:"uploaded_at"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UploadedAt time.Time `db:"uploaded_at" json:"uploaded_at"` + SecretsUpdatedAt *time.Time `db:"secrets_updated_at" json:"-"` } type MDMAppleRawDeclaration struct { @@ -697,10 +690,14 @@ type MDMAppleHostDeclaration struct { // Checksum contains the MD5 checksum of the declaration JSON uploaded // by the IT admin. Fleet uses this value as the ServerToken. Checksum string `db:"checksum" json:"-"` + + // SecretsUpdatedAt is the timestamp when the secrets were last updated or when this declaration was uploaded. + SecretsUpdatedAt *time.Time `db:"secrets_updated_at" json:"-"` } func (p MDMAppleHostDeclaration) Equal(other MDMAppleHostDeclaration) bool { statusEqual := p.Status == nil && other.Status == nil || p.Status != nil && other.Status != nil && *p.Status == *other.Status + secretsEqual := p.SecretsUpdatedAt == nil && other.SecretsUpdatedAt == nil || p.SecretsUpdatedAt != nil && other.SecretsUpdatedAt != nil && p.SecretsUpdatedAt.Equal(*other.SecretsUpdatedAt) return statusEqual && p.HostUUID == other.HostUUID && p.DeclarationUUID == other.DeclarationUUID && @@ -708,7 +705,8 @@ func (p MDMAppleHostDeclaration) Equal(other MDMAppleHostDeclaration) bool { p.Identifier == other.Identifier && p.OperationType == other.OperationType && p.Detail == other.Detail && - p.Checksum == other.Checksum + p.Checksum == other.Checksum && + secretsEqual } func NewMDMAppleDeclaration(raw []byte, teamID *uint, name string, declType, ident string) *MDMAppleDeclaration { @@ -733,8 +731,9 @@ type MDMAppleDDMTokensResponse struct { // // https://developer.apple.com/documentation/devicemanagement/synchronizationtokens type MDMAppleDDMDeclarationsToken struct { - DeclarationsToken string `db:"checksum"` - Timestamp time.Time `db:"latest_created_timestamp"` + DeclarationsToken string `db:"token"` + // Timestamp must JSON marshal to format YYYY-mm-ddTHH:MM:SSZ + Timestamp time.Time `db:"latest_created_timestamp"` } // MDMAppleDDMDeclarationItemsResponse is the response from the DDM declaration items endpoint. @@ -770,7 +769,7 @@ type MDMAppleDDMManifest struct { // https://developer.apple.com/documentation/devicemanagement/declarationitemsresponse type MDMAppleDDMDeclarationItem struct { Identifier string `db:"identifier"` - ServerToken string `db:"checksum"` + ServerToken string `db:"token"` } // MDMAppleDDMDeclarationResponse represents a declaration in the datastore. It is used for the DDM diff --git a/server/fleet/apple_mdm_test.go b/server/fleet/apple_mdm_test.go index 924e51aa874f..80e914eb41ca 100644 --- a/server/fleet/apple_mdm_test.go +++ b/server/fleet/apple_mdm_test.go @@ -472,6 +472,8 @@ func TestMDMAppleHostDeclarationEqual(t *testing.T) { fieldsInEqualMethod++ items[1].Status = &status1 fieldsInEqualMethod++ + items[1].SecretsUpdatedAt = items[0].SecretsUpdatedAt + fieldsInEqualMethod++ assert.Equal(t, fieldsInEqualMethod, numberOfFields, "MDMAppleHostDeclaration.Equal needs to be updated for new/updated field(s)") assert.True(t, items[0].Equal(items[1])) @@ -481,85 +483,6 @@ func TestMDMAppleHostDeclarationEqual(t *testing.T) { assert.True(t, items[0].Equal(items[1])) } -func TestMDMAppleProfilePayloadEqual(t *testing.T) { - t.Parallel() - - // This test is intended to ensure that the Equal method on MDMAppleProfilePayload is updated when new fields are added. - // The Equal method is used to identify whether database update is needed. - - items := [...]MDMAppleProfilePayload{{}, {}} - - numberOfFields := 0 - for i := 0; i < len(items); i++ { - rValue := reflect.ValueOf(&items[i]).Elem() - numberOfFields = rValue.NumField() - for j := 0; j < numberOfFields; j++ { - field := rValue.Field(j) - switch field.Kind() { - case reflect.String: - valueToSet := fmt.Sprintf("test %d", i) - field.SetString(valueToSet) - case reflect.Int: - field.SetInt(int64(i)) - case reflect.Bool: - field.SetBool(i%2 == 0) - case reflect.Pointer: - field.Set(reflect.New(field.Type().Elem())) - case reflect.Slice: - switch field.Type().Elem().Kind() { - case reflect.Uint8: - valueToSet := []byte("test") - field.Set(reflect.ValueOf(valueToSet)) - default: - t.Fatalf("unhandled slice type %s", field.Type().Elem().Kind()) - } - default: - t.Fatalf("unhandled field type %s", field.Kind()) - } - } - } - - status0 := MDMDeliveryStatus("status") - status1 := MDMDeliveryStatus("status") - items[0].Status = &status0 - checksum0 := []byte("checksum") - checksum1 := []byte("checksum") - items[0].Checksum = checksum0 - assert.False(t, items[0].Equal(items[1])) - - // Set known fields to be equal - fieldsInEqualMethod := 0 - items[1].ProfileUUID = items[0].ProfileUUID - fieldsInEqualMethod++ - items[1].ProfileIdentifier = items[0].ProfileIdentifier - fieldsInEqualMethod++ - items[1].ProfileName = items[0].ProfileName - fieldsInEqualMethod++ - items[1].HostUUID = items[0].HostUUID - fieldsInEqualMethod++ - items[1].HostPlatform = items[0].HostPlatform - fieldsInEqualMethod++ - items[1].Checksum = checksum1 - fieldsInEqualMethod++ - items[1].Status = &status1 - fieldsInEqualMethod++ - items[1].OperationType = items[0].OperationType - fieldsInEqualMethod++ - items[1].Detail = items[0].Detail - fieldsInEqualMethod++ - items[1].CommandUUID = items[0].CommandUUID - fieldsInEqualMethod++ - assert.Equal(t, fieldsInEqualMethod, numberOfFields, "MDMAppleProfilePayload.Equal needs to be updated for new/updated field(s)") - assert.True(t, items[0].Equal(items[1])) - - // Set pointers and slices to nil - items[0].Status = nil - items[1].Status = nil - items[0].Checksum = nil - items[1].Checksum = nil - assert.True(t, items[0].Equal(items[1])) -} - func TestConfigurationProfileLabelEqual(t *testing.T) { t.Parallel() diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 3991a8285893..78175335626c 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -1899,6 +1899,10 @@ type Datastore interface { // ExpandEmbeddedSecrets expands the fleet secrets in a // document using the secrets stored in the datastore. ExpandEmbeddedSecrets(ctx context.Context, document string) (string, error) + + // ExpandEmbeddedSecretsAndUpdatedAt is like ExpandEmbeddedSecrets but also + // returns the latest updated_at time of the secrets used in the expansion. + ExpandEmbeddedSecretsAndUpdatedAt(ctx context.Context, document string) (string, *time.Time, error) } // MDMAppleStore wraps nanomdm's storage and adds methods to deal with diff --git a/server/fleet/mdm.go b/server/fleet/mdm.go index b441067398b8..0690d5ed96bd 100644 --- a/server/fleet/mdm.go +++ b/server/fleet/mdm.go @@ -437,10 +437,11 @@ type MDMProfileBatchPayload struct { // Deprecated: Labels is the backwards-compatible way of specifying // LabelsIncludeAll. - Labels []string `json:"labels,omitempty"` - LabelsIncludeAll []string `json:"labels_include_all,omitempty"` - LabelsIncludeAny []string `json:"labels_include_any,omitempty"` - LabelsExcludeAny []string `json:"labels_exclude_any,omitempty"` + Labels []string `json:"labels,omitempty"` + LabelsIncludeAll []string `json:"labels_include_all,omitempty"` + LabelsIncludeAny []string `json:"labels_include_any,omitempty"` + LabelsExcludeAny []string `json:"labels_exclude_any,omitempty"` + SecretsUpdatedAt *time.Time `json:"-"` } func NewMDMConfigProfilePayloadFromWindows(cp *MDMWindowsConfigProfile) *MDMConfigProfilePayload { diff --git a/server/fleet/secret_variables.go b/server/fleet/secret_variables.go index c60557f1a14e..32ca9b530dce 100644 --- a/server/fleet/secret_variables.go +++ b/server/fleet/secret_variables.go @@ -1,8 +1,11 @@ package fleet +import "time" + type SecretVariable struct { - Name string `json:"name" db:"name"` - Value string `json:"value" db:"value"` + Name string `json:"name" db:"name"` + Value string `json:"value" db:"value"` + UpdatedAt time.Time `json:"-" db:"updated_at"` } func (h SecretVariable) AuthzType() string { diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index d87127fd174e..f56e54e43081 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -1185,6 +1185,8 @@ type ValidateEmbeddedSecretsFunc func(ctx context.Context, documents []string) e type ExpandEmbeddedSecretsFunc func(ctx context.Context, document string) (string, error) +type ExpandEmbeddedSecretsAndUpdatedAtFunc func(ctx context.Context, document string) (string, *time.Time, error) + type DataStore struct { HealthCheckFunc HealthCheckFunc HealthCheckFuncInvoked bool @@ -2932,6 +2934,9 @@ type DataStore struct { ExpandEmbeddedSecretsFunc ExpandEmbeddedSecretsFunc ExpandEmbeddedSecretsFuncInvoked bool + ExpandEmbeddedSecretsAndUpdatedAtFunc ExpandEmbeddedSecretsAndUpdatedAtFunc + ExpandEmbeddedSecretsAndUpdatedAtFuncInvoked bool + mu sync.Mutex } @@ -4906,7 +4911,7 @@ func (s *DataStore) UpdateCronStats(ctx context.Context, id int, status fleet.Cr s.mu.Lock() s.UpdateCronStatsFuncInvoked = true s.mu.Unlock() - return s.UpdateCronStatsFunc(ctx, id, status, &fleet.CronScheduleErrors{}) + return s.UpdateCronStatsFunc(ctx, id, status, cronErrors) } func (s *DataStore) UpdateAllCronStatsForInstance(ctx context.Context, instance string, fromStatus fleet.CronStatsStatus, toStatus fleet.CronStatsStatus) error { @@ -7008,3 +7013,10 @@ func (s *DataStore) ExpandEmbeddedSecrets(ctx context.Context, document string) s.mu.Unlock() return s.ExpandEmbeddedSecretsFunc(ctx, document) } + +func (s *DataStore) ExpandEmbeddedSecretsAndUpdatedAt(ctx context.Context, document string) (string, *time.Time, error) { + s.mu.Lock() + s.ExpandEmbeddedSecretsAndUpdatedAtFuncInvoked = true + s.mu.Unlock() + return s.ExpandEmbeddedSecretsAndUpdatedAtFunc(ctx, document) +} diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index aea85c0f9bd5..4b2d211f51a0 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -381,7 +381,7 @@ func (svc *Service) NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r } // Expand and validate secrets in profile - expanded, err := svc.ds.ExpandEmbeddedSecrets(ctx, string(b)) + expanded, secretsUpdatedAt, err := svc.ds.ExpandEmbeddedSecretsAndUpdatedAt(ctx, string(b)) if err != nil { return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("profile", err.Error())) } @@ -403,6 +403,7 @@ func (svc *Service) NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r // Save the original unexpanded profile cp.Mobileconfig = b + cp.SecretsUpdatedAt = secretsUpdatedAt labelMap, err := svc.validateProfileLabels(ctx, labels) if err != nil { @@ -512,7 +513,7 @@ func (svc *Service) NewMDMAppleDeclaration(ctx context.Context, teamID uint, r i return nil, err } - dataWithSecrets, err := svc.ds.ExpandEmbeddedSecrets(ctx, string(data)) + dataWithSecrets, secretsUpdatedAt, err := svc.ds.ExpandEmbeddedSecretsAndUpdatedAt(ctx, string(data)) if err != nil { return nil, fleet.NewInvalidArgumentError("profile", err.Error()) } @@ -533,6 +534,7 @@ func (svc *Service) NewMDMAppleDeclaration(ctx context.Context, teamID uint, r i } d := fleet.NewMDMAppleDeclaration(data, tmID, name, rawDecl.Type, rawDecl.Identifier) + d.SecretsUpdatedAt = secretsUpdatedAt switch labelsMembershipMode { case fleet.LabelsIncludeAny: @@ -1989,7 +1991,7 @@ func (svc *Service) BatchSetMDMAppleProfiles(ctx context.Context, tmID *uint, tm ) } // Expand profile for validation - expanded, err := svc.ds.ExpandEmbeddedSecrets(ctx, string(prof)) + expanded, secretsUpdatedAt, err := svc.ds.ExpandEmbeddedSecretsAndUpdatedAt(ctx, string(prof)) if err != nil { return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError(fmt.Sprintf("profiles[%d]", i), err.Error()), @@ -2009,6 +2011,7 @@ func (svc *Service) BatchSetMDMAppleProfiles(ctx context.Context, tmID *uint, tm // Store original unexpanded profile mdmProf.Mobileconfig = prof + mdmProf.SecretsUpdatedAt = secretsUpdatedAt if byName[mdmProf.Name] { return ctxerr.Wrap(ctx, @@ -3422,6 +3425,7 @@ func ReconcileAppleProfiles( ProfileIdentifier: p.ProfileIdentifier, ProfileName: p.ProfileName, Checksum: p.Checksum, + SecretsUpdatedAt: p.SecretsUpdatedAt, OperationType: pp.OperationType, Status: pp.Status, CommandUUID: pp.CommandUUID, @@ -3453,6 +3457,7 @@ func ReconcileAppleProfiles( ProfileIdentifier: p.ProfileIdentifier, ProfileName: p.ProfileName, Checksum: p.Checksum, + SecretsUpdatedAt: p.SecretsUpdatedAt, } hostProfiles = append(hostProfiles, hostProfile) hostProfilesToInstallMap[hostProfileUUID{HostUUID: p.HostUUID, ProfileUUID: p.ProfileUUID}] = hostProfile @@ -3490,6 +3495,7 @@ func ReconcileAppleProfiles( ProfileIdentifier: p.ProfileIdentifier, ProfileName: p.ProfileName, Checksum: p.Checksum, + SecretsUpdatedAt: p.SecretsUpdatedAt, }) } @@ -4177,6 +4183,9 @@ func (svc *MDMAppleDDMService) handleTokens(ctx context.Context, hostUUID string return nil, ctxerr.Wrap(ctx, err, "getting synchronization tokens") } + // Important: Timestamp must use format YYYY-mm-ddTHH:MM:SSZ (no milliseconds) + // Source: https://developer.apple.com/documentation/devicemanagement/synchronizationtokens?language=objc + tok.Timestamp = tok.Timestamp.Truncate(time.Second) b, err := json.Marshal(fleet.MDMAppleDDMTokensResponse{ SyncTokens: *tok, }) @@ -4262,7 +4271,7 @@ func (svc *MDMAppleDDMService) handleActivationDeclaration(ctx context.Context, }, "ServerToken": "%s", "Type": "com.apple.activation.simple" -}`, parts[2], references, d.Checksum) +}`, parts[2], references, d.Token) return []byte(response), nil } @@ -4285,7 +4294,7 @@ func (svc *MDMAppleDDMService) handleConfigurationDeclaration(ctx context.Contex if err := json.Unmarshal([]byte(expanded), &tempd); err != nil { return nil, ctxerr.Wrap(ctx, err, "unmarshaling stored declaration") } - tempd["ServerToken"] = d.Checksum + tempd["ServerToken"] = d.Token b, err := json.Marshal(tempd) if err != nil { diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index 4722fe3bd489..ac9780d8863b 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -215,6 +215,9 @@ func setupAppleMDMService(t *testing.T, license *fleet.LicenseInfo) (fleet.Servi ds.ExpandEmbeddedSecretsFunc = func(ctx context.Context, document string) (string, error) { return document, nil } + ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, document string) (string, *time.Time, error) { + return document, nil, nil + } apnsCert, apnsKey, err := mysql.GenerateTestCertBytes() require.NoError(t, err) crt, key, err := apple_mdm.NewSCEPCACertKey() diff --git a/server/service/integration_mdm_ddm_test.go b/server/service/integration_mdm_ddm_test.go index 91b23f2a466a..d0faabe406ce 100644 --- a/server/service/integration_mdm_ddm_test.go +++ b/server/service/integration_mdm_ddm_test.go @@ -472,22 +472,23 @@ func (s *integrationMDMTestSuite) TestAppleDDMSecretVariables() { _, mdmDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t) checkDeclarationItemsResp := func(t *testing.T, r fleet.MDMAppleDDMDeclarationItemsResponse, expectedDeclTok string, - expectedDeclsByChecksum map[string]fleet.MDMAppleDeclaration) { + expectedDeclsByToken map[string]fleet.MDMAppleDeclaration) { require.Equal(t, expectedDeclTok, r.DeclarationsToken) require.NotEmpty(t, r.Declarations.Activations) require.Empty(t, r.Declarations.Assets) require.Empty(t, r.Declarations.Management) - require.Len(t, r.Declarations.Configurations, len(expectedDeclsByChecksum)) + require.Len(t, r.Declarations.Configurations, len(expectedDeclsByToken)) for _, m := range r.Declarations.Configurations { - d, ok := expectedDeclsByChecksum[m.ServerToken] - require.True(t, ok) + d, ok := expectedDeclsByToken[m.ServerToken] + if !ok { + for k := range expectedDeclsByToken { + t.Logf("expected token: %x", k) + } + } + require.True(t, ok, "server token %x not found for %s", m.ServerToken, m.Identifier) require.Equal(t, d.Identifier, m.Identifier) } } - calcChecksum := func(source []byte) string { - csum := fmt.Sprintf("%x", md5.Sum(source)) //nolint:gosec - return strings.ToUpper(csum) - } tmpl := ` { @@ -516,25 +517,15 @@ func (s *integrationMDMTestSuite) TestAppleDDMSecretVariables() { decls[1] = []byte(strings.ReplaceAll(string(decls[1]), myBash, "$"+fleet.ServerSecretPrefix+"BASH")) secretProfile := decls[2] decls[2] = []byte("${" + fleet.ServerSecretPrefix + "PROFILE}") - declsByChecksum := map[string]fleet.MDMAppleDeclaration{ - calcChecksum(decls[0]): { - Identifier: "com.fleet.config0", - }, - calcChecksum(decls[1]): { - Identifier: "com.fleet.config1", - }, - calcChecksum(decls[2]): { - Identifier: "com.fleet.config2", - }, - } // Create declarations - // First dry run - s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{ + profilesReq := batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{ {Name: "N0", Contents: decls[0]}, {Name: "N1", Contents: decls[1]}, {Name: "N2", Contents: decls[2]}, - }}, http.StatusNoContent, "dry_run", "true") + }} + // First dry run + s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", profilesReq, http.StatusNoContent, "dry_run", "true") var resp listMDMConfigProfilesResponse s.DoJSON("GET", "/api/latest/fleet/mdm/profiles", &listMDMConfigProfilesRequest{}, http.StatusOK, &resp) @@ -557,11 +548,7 @@ func (s *integrationMDMTestSuite) TestAppleDDMSecretVariables() { s.DoJSON("PUT", "/api/latest/fleet/spec/secret_variables", req, http.StatusOK, &secretResp) // Now real run - s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{ - {Name: "N0", Contents: decls[0]}, - {Name: "N1", Contents: decls[1]}, - {Name: "N2", Contents: decls[2]}, - }}, http.StatusNoContent) + s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", profilesReq, http.StatusNoContent) s.DoJSON("GET", "/api/latest/fleet/mdm/profiles", &listMDMConfigProfilesRequest{}, http.StatusOK, &resp) require.Len(t, resp.Profiles, len(decls)) @@ -585,9 +572,11 @@ SELECT identifier, name, raw_json, - checksum, + HEX(checksum) as checksum, + HEX(token) as token, created_at, - uploaded_at + uploaded_at, + secrets_updated_at FROM mdm_apple_declarations WHERE name = ?` @@ -599,19 +588,29 @@ WHERE name = ?` } nameToIdentifier := make(map[string]string, 3) nameToUUID := make(map[string]string, 3) + declsByToken := map[string]fleet.MDMAppleDeclaration{} decl := getDeclaration(t, "N0") nameToIdentifier["N0"] = decl.Identifier nameToUUID["N0"] = decl.DeclarationUUID + declsByToken[decl.Token] = fleet.MDMAppleDeclaration{ + Identifier: "com.fleet.config0", + } decl = getDeclaration(t, "N1") assert.NotContains(t, string(decl.RawJSON), myBash) assert.Contains(t, string(decl.RawJSON), "$"+fleet.ServerSecretPrefix+"BASH") nameToIdentifier["N1"] = decl.Identifier nameToUUID["N1"] = decl.DeclarationUUID + n1Token := decl.Token + declsByToken[decl.Token] = fleet.MDMAppleDeclaration{ + Identifier: "com.fleet.config1", + } decl = getDeclaration(t, "N2") assert.Equal(t, string(decl.RawJSON), "${"+fleet.ServerSecretPrefix+"PROFILE}") nameToIdentifier["N2"] = decl.Identifier nameToUUID["N2"] = decl.DeclarationUUID - + declsByToken[decl.Token] = fleet.MDMAppleDeclaration{ + Identifier: "com.fleet.config2", + } // trigger a profile sync s.awaitTriggerProfileSchedule(t) @@ -624,7 +623,7 @@ WHERE name = ?` r, err = mdmDevice.DeclarativeManagement("declaration-items") require.NoError(t, err) itemsResp := parseDeclarationItemsResp(t, r) - checkDeclarationItemsResp(t, itemsResp, currDeclToken, declsByChecksum) + checkDeclarationItemsResp(t, itemsResp, currDeclToken, declsByToken) // Now, retrieve the declaration configuration profiles declarationPath := fmt.Sprintf("declaration/configuration/%s", nameToIdentifier["N0"]) @@ -646,6 +645,82 @@ WHERE name = ?` require.NoError(t, json.NewDecoder(r.Body).Decode(&gotParsed)) assert.EqualValues(t, `{"DataAssetReference":"com.fleet.asset.bash","ServiceType":"com.apple.bash2"}`, gotParsed.Payload) + // Upload the same profiles again -- nothing should change + s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", profilesReq, http.StatusNoContent, "dry_run", "true") + s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", profilesReq, http.StatusNoContent) + s.awaitTriggerProfileSchedule(t) + // Get tokens again + r, err = mdmDevice.DeclarativeManagement("tokens") + require.NoError(t, err) + tokens = parseTokensResp(t, r) + currDeclToken = tokens.SyncTokens.DeclarationsToken + // Get declaration items -- the checksums should be the same as before + r, err = mdmDevice.DeclarativeManagement("declaration-items") + require.NoError(t, err) + itemsResp = parseDeclarationItemsResp(t, r) + checkDeclarationItemsResp(t, itemsResp, currDeclToken, declsByToken) + + // Change the secrets. + myBash = "my.new.bash" + req = secretVariablesRequest{ + SecretVariables: []fleet.SecretVariable{ + { + Name: "FLEET_SECRET_BASH", + Value: myBash, // changed + }, + { + Name: "FLEET_SECRET_PROFILE", + Value: string(secretProfile), // did not change + }, + }, + } + s.DoJSON("PUT", "/api/latest/fleet/spec/secret_variables", req, http.StatusOK, &secretResp) + s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", profilesReq, http.StatusNoContent, "dry_run", "true") + s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", profilesReq, http.StatusNoContent) + // The token of the declaration with the updated secret should have changed. + decl = getDeclaration(t, "N1") + assert.NotContains(t, string(decl.RawJSON), myBash) + assert.Contains(t, string(decl.RawJSON), "$"+fleet.ServerSecretPrefix+"BASH") + nameToIdentifier["N1"] = decl.Identifier + nameToUUID["N1"] = decl.DeclarationUUID + assert.NotEqual(t, n1Token, decl.Token) + // Update expected token + delete(declsByToken, n1Token) + declsByToken[decl.Token] = fleet.MDMAppleDeclaration{ + Identifier: "com.fleet.config1", + } + s.awaitTriggerProfileSchedule(t) + + // Get tokens again + r, err = mdmDevice.DeclarativeManagement("tokens") + require.NoError(t, err) + tokens = parseTokensResp(t, r) + currDeclToken = tokens.SyncTokens.DeclarationsToken + // Only N1 should have changed + r, err = mdmDevice.DeclarativeManagement("declaration-items") + require.NoError(t, err) + itemsResp = parseDeclarationItemsResp(t, r) + checkDeclarationItemsResp(t, itemsResp, currDeclToken, declsByToken) + + // Now, retrieve the declaration configuration profiles + declarationPath = fmt.Sprintf("declaration/configuration/%s", nameToIdentifier["N0"]) + r, err = mdmDevice.DeclarativeManagement(declarationPath) + require.NoError(t, err) + require.NoError(t, json.NewDecoder(r.Body).Decode(&gotParsed)) + assert.EqualValues(t, `{"DataAssetReference":"com.fleet.asset.bash","ServiceType":"com.apple.bash0"}`, gotParsed.Payload) + + declarationPath = fmt.Sprintf("declaration/configuration/%s", nameToIdentifier["N1"]) + r, err = mdmDevice.DeclarativeManagement(declarationPath) + require.NoError(t, err) + require.NoError(t, json.NewDecoder(r.Body).Decode(&gotParsed)) + assert.EqualValues(t, `{"DataAssetReference":"com.fleet.asset.bash","ServiceType":"my.new.bash"}`, gotParsed.Payload) + + declarationPath = fmt.Sprintf("declaration/configuration/%s", nameToIdentifier["N2"]) + r, err = mdmDevice.DeclarativeManagement(declarationPath) + require.NoError(t, err) + require.NoError(t, json.NewDecoder(r.Body).Decode(&gotParsed)) + assert.EqualValues(t, `{"DataAssetReference":"com.fleet.asset.bash","ServiceType":"com.apple.bash2"}`, gotParsed.Payload) + // Delete the profiles s.Do("DELETE", "/api/latest/fleet/configuration_profiles/"+nameToUUID["N0"], nil, http.StatusOK) s.Do("DELETE", "/api/latest/fleet/configuration_profiles/"+nameToUUID["N1"], nil, http.StatusOK) diff --git a/server/service/integration_mdm_profiles_test.go b/server/service/integration_mdm_profiles_test.go index 9f626d930070..976be8a815db 100644 --- a/server/service/integration_mdm_profiles_test.go +++ b/server/service/integration_mdm_profiles_test.go @@ -259,12 +259,47 @@ func (s *integrationMDMTestSuite) TestAppleProfileManagement() { s.checkMDMProfilesSummaries(t, nil, expectedNoTeamSummary, &expectedNoTeamSummary) // empty because host was transferred s.checkMDMProfilesSummaries(t, &tm.ID, expectedTeamSummary, &expectedTeamSummary) // host still verifying team profiles - // with no changes + // Upload the same profiles again. No changes expected. + s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: teamProfiles}, http.StatusNoContent, + "team_id", fmt.Sprint(tm.ID)) s.awaitTriggerProfileSchedule(t) installs, removes = checkNextPayloads(t, mdmDevice, false) require.Empty(t, installs) require.Empty(t, removes) + // Change the secret variable and upload the profiles again. We should see the profile with updated secret installed. + secretName = "newSecretName" + req = secretVariablesRequest{ + SecretVariables: []fleet.SecretVariable{ + { + Name: "FLEET_SECRET_NAME", + Value: secretName, // changed + }, + { + Name: "FLEET_SECRET_PROFILE", + Value: secretProfile, // did not change + }, + }, + } + s.DoJSON("PUT", "/api/latest/fleet/spec/secret_variables", req, http.StatusOK, &secretResp) + s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: teamProfiles}, http.StatusNoContent, + "team_id", fmt.Sprint(tm.ID)) + s.awaitTriggerProfileSchedule(t) + installs, removes = checkNextPayloads(t, mdmDevice, false) + // Manually replace the expected secret variables in the profile + wantTeamProfilesChanged := [][]byte{ + teamProfiles[1], + } + wantTeamProfilesChanged[0] = []byte(strings.ReplaceAll(string(wantTeamProfilesChanged[0]), "$FLEET_SECRET_IDENTIFIER", + secretIdentifier)) + wantTeamProfilesChanged[0] = []byte(strings.ReplaceAll(string(wantTeamProfilesChanged[0]), "${FLEET_SECRET_TYPE}", secretType)) + wantTeamProfilesChanged[0] = []byte(strings.ReplaceAll(string(wantTeamProfilesChanged[0]), "$FLEET_SECRET_NAME", secretName)) + // verify that we should install the team profiles + s.signedProfilesMatch(wantTeamProfilesChanged, installs) + wantTeamProfiles[1] = wantTeamProfilesChanged[0] + // No profiles should be deleted + assert.Empty(t, removes) + // Clear the profiles using the new (non-deprecated) endpoint. s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: nil}, http.StatusNoContent, "team_id", fmt.Sprint(tm.ID), "dry_run", "true") @@ -295,6 +330,47 @@ func (s *integrationMDMTestSuite) TestAppleProfileManagement() { // verify that we should install the team profiles s.signedProfilesMatch(wantTeamProfiles, installs) + // Upload the same profiles again. No changes expected. + s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchRequest, http.StatusNoContent, "team_id", fmt.Sprint(tm.ID), "dry_run", "true") + s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchRequest, http.StatusNoContent, "team_id", fmt.Sprint(tm.ID)) + s.awaitTriggerProfileSchedule(t) + installs, removes = checkNextPayloads(t, mdmDevice, false) + require.Empty(t, installs) + require.Empty(t, removes) + + // Change the secret variable and upload the profiles again. We should see the profile with updated secret installed. + secretName = "new2SecretName" + req = secretVariablesRequest{ + SecretVariables: []fleet.SecretVariable{ + { + Name: "FLEET_SECRET_NAME", + Value: secretName, // changed + }, + { + Name: "FLEET_SECRET_PROFILE", + Value: secretProfile, // did not change + }, + }, + } + s.DoJSON("PUT", "/api/latest/fleet/spec/secret_variables", req, http.StatusOK, &secretResp) + s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchRequest, http.StatusNoContent, "team_id", fmt.Sprint(tm.ID), "dry_run", "true") + s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchRequest, http.StatusNoContent, "team_id", fmt.Sprint(tm.ID)) + s.awaitTriggerProfileSchedule(t) + installs, removes = checkNextPayloads(t, mdmDevice, false) + // Manually replace the expected secret variables in the profile + wantTeamProfilesChanged = [][]byte{ + teamProfiles[1], + } + wantTeamProfilesChanged[0] = []byte(strings.ReplaceAll(string(wantTeamProfilesChanged[0]), "$FLEET_SECRET_IDENTIFIER", + secretIdentifier)) + wantTeamProfilesChanged[0] = []byte(strings.ReplaceAll(string(wantTeamProfilesChanged[0]), "${FLEET_SECRET_TYPE}", secretType)) + wantTeamProfilesChanged[0] = []byte(strings.ReplaceAll(string(wantTeamProfilesChanged[0]), "$FLEET_SECRET_NAME", secretName)) + // verify that we should install the team profiles + s.signedProfilesMatch(wantTeamProfilesChanged, installs) + wantTeamProfiles[1] = wantTeamProfilesChanged[0] + // No profiles should be deleted + assert.Empty(t, removes) + var hostResp getHostResponse s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d", host.ID), getHostRequest{}, http.StatusOK, &hostResp) require.NotEmpty(t, hostResp.Host.MDM.Profiles) @@ -5291,6 +5367,9 @@ func (s *integrationMDMTestSuite) TestAppleDDMSecretVariablesUpload() { getProfileContents := func(profileUUID string) string { profile, err := s.ds.GetMDMAppleDeclaration(context.Background(), profileUUID) require.NoError(s.T(), err) + // Since our DDM profiles contain secrets, the checksum and token should be different + assert.NotNil(s.T(), profile.SecretsUpdatedAt) + assert.NotEqual(s.T(), profile.Checksum, profile.Token) return string(profile.RawJSON) } @@ -5424,6 +5503,7 @@ func (s *integrationMDMTestSuite) TestAppleConfigSecretVariablesUpload() { getProfileContents := func(profileUUID string) string { profile, err := s.ds.GetMDMAppleConfigProfile(context.Background(), profileUUID) require.NoError(s.T(), err) + assert.NotNil(s.T(), profile.SecretsUpdatedAt) return string(profile.Mobileconfig) } diff --git a/server/service/mdm.go b/server/service/mdm.go index 8fdc0da52b3a..406c79441777 100644 --- a/server/service/mdm.go +++ b/server/service/mdm.go @@ -1636,10 +1636,11 @@ func (svc *Service) BatchSetMDMProfiles( // In order to map the expanded profiles back to the original profiles, we will use the index. profilesWithSecrets := make(map[int]fleet.MDMProfileBatchPayload, len(profiles)) for i, p := range profiles { - expanded, err := svc.ds.ExpandEmbeddedSecrets(ctx, string(p.Contents)) + expanded, secretsUpdatedAt, err := svc.ds.ExpandEmbeddedSecretsAndUpdatedAt(ctx, string(p.Contents)) if err != nil { return err } + p.SecretsUpdatedAt = secretsUpdatedAt pCopy := p // If the profile does not contain secrets, then expanded and original content point to the same slice/memory location. pCopy.Contents = []byte(expanded) @@ -1912,6 +1913,7 @@ func getAppleProfiles( } mdmDecl := fleet.NewMDMAppleDeclaration(prof.Contents, tmID, prof.Name, rawDecl.Type, rawDecl.Identifier) + mdmDecl.SecretsUpdatedAt = prof.SecretsUpdatedAt for _, labelName := range prof.LabelsIncludeAll { if lbl, ok := labelMap[labelName]; ok { declLabel := fleet.ConfigurationProfileLabel{ @@ -1967,6 +1969,7 @@ func getAppleProfiles( } mdmProf, err := fleet.NewMDMAppleConfigProfile(prof.Contents, tmID) + mdmProf.SecretsUpdatedAt = prof.SecretsUpdatedAt if err != nil { return nil, nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError(prof.Name, err.Error()), diff --git a/server/service/mdm_test.go b/server/service/mdm_test.go index 66f5e411e11c..d7cf53fca5d3 100644 --- a/server/service/mdm_test.go +++ b/server/service/mdm_test.go @@ -1294,8 +1294,8 @@ func TestMDMBatchSetProfiles(t *testing.T) { ds.ValidateEmbeddedSecretsFunc = func(ctx context.Context, documents []string) error { return nil } - ds.ExpandEmbeddedSecretsFunc = func(ctx context.Context, document string) (string, error) { - return document, nil + ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, document string) (string, *time.Time, error) { + return document, nil, nil } testCases := []struct { @@ -2113,8 +2113,8 @@ func TestBatchSetMDMProfilesLabels(t *testing.T) { ds.ValidateEmbeddedSecretsFunc = func(ctx context.Context, documents []string) error { return nil } - ds.ExpandEmbeddedSecretsFunc = func(ctx context.Context, document string) (string, error) { - return document, nil + ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, document string) (string, *time.Time, error) { + return document, nil, nil } profiles := []fleet.MDMProfileBatchPayload{