diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index 700193932f..718a0e2ca8 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -1485,7 +1485,7 @@ the way that the Fleet server works. mdmCheckinAndCommandService.RegisterResultsHandler("InstalledApplicationList", service.NewInstalledApplicationListResultsHandler(ds, commander, logger, config.Server.VPPVerifyTimeout, config.Server.VPPVerifyRequestDelay, svc.NewActivity)) mdmCheckinAndCommandService.RegisterResultsHandler(fleet.DeviceLocationCmdName, service.NewDeviceLocationResultsHandler(ds, commander, logger)) - mdmCheckinAndCommandService.RegisterResultsHandler(fleet.SetRecoveryLockCmdName, service.NewSetRecoveryLockResultsHandler(ds, logger)) + mdmCheckinAndCommandService.RegisterResultsHandler(fleet.SetRecoveryLockCmdName, service.NewSetRecoveryLockResultsHandler(ds, logger, svc.NewActivity)) hasSCEPChallenge, err := checkMDMAssets([]fleet.MDMAssetName{fleet.MDMAssetSCEPChallenge}) if err != nil { diff --git a/ee/server/service/teams.go b/ee/server/service/teams.go index b694fa9d91..56ff97fe09 100644 --- a/ee/server/service/teams.go +++ b/ee/server/service/teams.go @@ -486,9 +486,9 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T if recoveryLockPasswordUpdated { var act fleet.ActivityDetails if team.Config.MDM.EnableRecoveryLockPassword { - act = fleet.ActivityTypeEnabledRecoveryLockPassword{TeamID: &team.ID, TeamName: &team.Name} + act = fleet.ActivityTypeEnabledRecoveryLockPasswords{TeamID: &team.ID, TeamName: &team.Name} } else { - act = fleet.ActivityTypeDisabledRecoveryLockPassword{TeamID: &team.ID, TeamName: &team.Name} + act = fleet.ActivityTypeDisabledRecoveryLockPasswords{TeamID: &team.ID, TeamName: &team.Name} } if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil { return nil, ctxerr.Wrap(ctx, err, "create activity for team recovery lock password") @@ -1401,6 +1401,16 @@ func (svc *Service) createTeamFromSpec( return nil, ctxerr.Wrap(ctx, err, "create activity for team macos disk encryption") } } + + if spec.MDM.EnableRecoveryLockPassword.Value && appCfg.MDM.EnabledAndConfigured { + if err := svc.NewActivity( + ctx, + authz.UserFromContext(ctx), + fleet.ActivityTypeEnabledRecoveryLockPasswords{TeamID: &tm.ID, TeamName: &tm.Name}, + ); err != nil { + return nil, ctxerr.Wrap(ctx, err, "create activity for team recovery lock password") + } + } return tm, nil } @@ -1487,9 +1497,10 @@ func (svc *Service) editTeamFromSpec( team.Config.MDM.RequireBitLockerPIN = spec.MDM.RequireBitLockerPIN.Value } + var didUpdateRecoveryLockPassword bool if spec.MDM.EnableRecoveryLockPassword.Valid { - recoveryLockPasswordUpdated := team.Config.MDM.EnableRecoveryLockPassword != spec.MDM.EnableRecoveryLockPassword.Value - if recoveryLockPasswordUpdated && !appCfg.MDM.EnabledAndConfigured { + didUpdateRecoveryLockPassword = team.Config.MDM.EnableRecoveryLockPassword != spec.MDM.EnableRecoveryLockPassword.Value + if didUpdateRecoveryLockPassword && !appCfg.MDM.EnabledAndConfigured { return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("mdm.enable_recovery_lock_password", `Couldn't update enable_recovery_lock_password because MDM features aren't turned on in Fleet.`)) } @@ -1720,6 +1731,18 @@ func (svc *Service) editTeamFromSpec( } } + if appCfg.MDM.EnabledAndConfigured && didUpdateRecoveryLockPassword { + var act fleet.ActivityDetails + if team.Config.MDM.EnableRecoveryLockPassword { + act = fleet.ActivityTypeEnabledRecoveryLockPasswords{TeamID: &team.ID, TeamName: &team.Name} + } else { + act = fleet.ActivityTypeDisabledRecoveryLockPasswords{TeamID: &team.ID, TeamName: &team.Name} + } + if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil { + return ctxerr.Wrap(ctx, err, "create activity for team recovery lock password") + } + } + // if the macos setup assistant was cleared, remove it for that team if spec.MDM.MacOSSetup.MacOSSetupAssistant.Set && spec.MDM.MacOSSetup.MacOSSetupAssistant.Value == "" && diff --git a/server/fleet/activities.go b/server/fleet/activities.go index 451132b72c..e04dbd49be 100644 --- a/server/fleet/activities.go +++ b/server/fleet/activities.go @@ -123,8 +123,9 @@ var ActivityDetailsList = []ActivityDetails{ ActivityTypeEnabledMacosDiskEncryption{}, ActivityTypeDisabledMacosDiskEncryption{}, - ActivityTypeEnabledRecoveryLockPassword{}, - ActivityTypeDisabledRecoveryLockPassword{}, + ActivityTypeSetHostRecoveryLockPassword{}, + ActivityTypeEnabledRecoveryLockPasswords{}, + ActivityTypeDisabledRecoveryLockPasswords{}, ActivityTypeEnabledGitOpsMode{}, ActivityTypeDisabledGitOpsMode{}, @@ -755,22 +756,39 @@ func (a ActivityTypeDisabledMacosDiskEncryption) ActivityName() string { return "disabled_macos_disk_encryption" } -type ActivityTypeEnabledRecoveryLockPassword struct { +type ActivityTypeSetHostRecoveryLockPassword struct { + HostID uint `json:"host_id"` + HostDisplayName string `json:"host_display_name"` +} + +func (a ActivityTypeSetHostRecoveryLockPassword) ActivityName() string { + return "set_host_recovery_lock_password" +} + +func (a ActivityTypeSetHostRecoveryLockPassword) HostIDs() []uint { + return []uint{a.HostID} +} + +func (a ActivityTypeSetHostRecoveryLockPassword) WasFromAutomation() bool { + return true +} + +type ActivityTypeEnabledRecoveryLockPasswords struct { TeamID *uint `json:"team_id" renameto:"fleet_id"` TeamName *string `json:"team_name" renameto:"fleet_name"` } -func (a ActivityTypeEnabledRecoveryLockPassword) ActivityName() string { - return "enabled_recovery_lock_password" +func (a ActivityTypeEnabledRecoveryLockPasswords) ActivityName() string { + return "enabled_recovery_lock_passwords" } -type ActivityTypeDisabledRecoveryLockPassword struct { +type ActivityTypeDisabledRecoveryLockPasswords struct { TeamID *uint `json:"team_id" renameto:"fleet_id"` TeamName *string `json:"team_name" renameto:"fleet_name"` } -func (a ActivityTypeDisabledRecoveryLockPassword) ActivityName() string { - return "disabled_recovery_lock_password" +func (a ActivityTypeDisabledRecoveryLockPasswords) ActivityName() string { + return "disabled_recovery_lock_passwords" } type ActivityTypeEnabledGitOpsMode struct{} diff --git a/server/service/appconfig.go b/server/service/appconfig.go index 0d776b420d..0efe19de8c 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -1096,9 +1096,9 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle if oldAppConfig.MDM.EnabledAndConfigured { var act fleet.ActivityDetails if appConfig.MDM.EnableRecoveryLockPassword.Value { - act = fleet.ActivityTypeEnabledRecoveryLockPassword{} + act = fleet.ActivityTypeEnabledRecoveryLockPasswords{} } else { - act = fleet.ActivityTypeDisabledRecoveryLockPassword{} + act = fleet.ActivityTypeDisabledRecoveryLockPasswords{} } if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil { return nil, ctxerr.Wrap(ctx, err, "create activity for app config recovery lock password") diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index be6a6b1475..9d57eb52b8 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -7336,6 +7336,7 @@ func NewRecoveryLockResult(cmdResult *mdm.CommandResults) fleet.MDMCommandResult func NewSetRecoveryLockResultsHandler( ds fleet.Datastore, logger *slog.Logger, + newActivityFn fleet.NewActivityFunc, ) fleet.MDMCommandResultsHandler { return func(ctx context.Context, results fleet.MDMCommandResults) error { // Get the underlying result to access status and error chain @@ -7383,8 +7384,35 @@ func NewSetRecoveryLockResultsHandler( if err := ds.SetRecoveryLockVerified(ctx, hostUUID); err != nil { return ctxerr.Wrap(ctx, err, "SetRecoveryLock handler: set recovery lock verified") } + + // Get host info for activity logging - don't fail the operation if this fails + var hostID uint + var displayName string + host, err := ds.HostLiteByIdentifier(ctx, hostUUID) + if err != nil { + logger.WarnContext(ctx, "SetRecoveryLock handler: failed to get host for activity logging", + "host_uuid", hostUUID, + "err", err, + ) + } else { + hostID = host.ID + displayName = host.Hostname + + // Log the activity only if we could identify the host (fleet-initiated via WasFromAutomation) + if err := newActivityFn(ctx, nil, fleet.ActivityTypeSetHostRecoveryLockPassword{ + HostID: hostID, + HostDisplayName: displayName, + }); err != nil { + logger.WarnContext(ctx, "SetRecoveryLock handler: failed to create activity", + "host_uuid", hostUUID, + "err", err, + ) + } + } + logger.InfoContext(ctx, "SetRecoveryLock acknowledged, marked verified", "host_uuid", hostUUID, + "host_id", hostID, ) } diff --git a/server/service/apple_mdm_cmd_results_test.go b/server/service/apple_mdm_cmd_results_test.go index d17fea7b7d..9d78a95d30 100644 --- a/server/service/apple_mdm_cmd_results_test.go +++ b/server/service/apple_mdm_cmd_results_test.go @@ -306,7 +306,24 @@ func TestSetRecoveryLockResultsHandler(t *testing.T) { return nil } - handler := NewSetRecoveryLockResultsHandler(ds, logger) + ds.HostLiteByIdentifierFunc = func(_ context.Context, identifier string) (*fleet.HostLite, error) { + assert.Equal(t, hostUUID, identifier) + return &fleet.HostLite{ID: 1, Hostname: "Test Host"}, nil + } + + var activityCalled bool + var capturedHostID uint + var capturedDisplayName string + newActivityFn := func(_ context.Context, _ *fleet.User, activity fleet.ActivityDetails) error { + activityCalled = true + act, ok := activity.(fleet.ActivityTypeSetHostRecoveryLockPassword) + require.True(t, ok) + capturedHostID = act.HostID + capturedDisplayName = act.HostDisplayName + return nil + } + + handler := NewSetRecoveryLockResultsHandler(ds, logger, newActivityFn) result := NewRecoveryLockResult(&mdm.CommandResults{ Enrollment: mdm.Enrollment{UDID: hostUUID}, @@ -320,6 +337,9 @@ func TestSetRecoveryLockResultsHandler(t *testing.T) { // Verify status was set to verified assert.True(t, verifiedCalled) + assert.True(t, activityCalled) + assert.Equal(t, uint(1), capturedHostID) + assert.Equal(t, "Test Host", capturedDisplayName) }) t.Run("error status sets failed", func(t *testing.T) { @@ -338,7 +358,12 @@ func TestSetRecoveryLockResultsHandler(t *testing.T) { return nil } - handler := NewSetRecoveryLockResultsHandler(ds, logger) + newActivityFn := func(_ context.Context, _ *fleet.User, _ fleet.ActivityDetails) error { + t.Fatal("activity should not be called on error") + return nil + } + + handler := NewSetRecoveryLockResultsHandler(ds, logger, newActivityFn) result := NewRecoveryLockResult(&mdm.CommandResults{ Enrollment: mdm.Enrollment{UDID: hostUUID}, @@ -368,7 +393,12 @@ func TestSetRecoveryLockResultsHandler(t *testing.T) { return nil } - handler := NewSetRecoveryLockResultsHandler(ds, logger) + newActivityFn := func(_ context.Context, _ *fleet.User, _ fleet.ActivityDetails) error { + t.Fatal("activity should not be called on error") + return nil + } + + handler := NewSetRecoveryLockResultsHandler(ds, logger, newActivityFn) result := NewRecoveryLockResult(&mdm.CommandResults{ Enrollment: mdm.Enrollment{UDID: hostUUID}, @@ -397,7 +427,11 @@ func TestSetRecoveryLockResultsHandler(t *testing.T) { return nil } - handler := NewSetRecoveryLockResultsHandler(ds, logger) + newActivityFn := func(_ context.Context, _ *fleet.User, _ fleet.ActivityDetails) error { + return nil + } + + handler := NewSetRecoveryLockResultsHandler(ds, logger, newActivityFn) result := NewRecoveryLockResult(&mdm.CommandResults{ Enrollment: mdm.Enrollment{UDID: hostUUID}, @@ -427,7 +461,11 @@ func TestSetRecoveryLockResultsHandler(t *testing.T) { return nil } - handler := NewSetRecoveryLockResultsHandler(ds, logger) + newActivityFn := func(_ context.Context, _ *fleet.User, _ fleet.ActivityDetails) error { + return nil + } + + handler := NewSetRecoveryLockResultsHandler(ds, logger, newActivityFn) // Test MDMClientError 70 (password not provided) result := NewRecoveryLockResult(&mdm.CommandResults{ @@ -459,7 +497,11 @@ func TestSetRecoveryLockResultsHandler(t *testing.T) { return nil } - handler := NewSetRecoveryLockResultsHandler(ds, logger) + newActivityFn := func(_ context.Context, _ *fleet.User, _ fleet.ActivityDetails) error { + return nil + } + + handler := NewSetRecoveryLockResultsHandler(ds, logger, newActivityFn) // Test ROSLockoutServiceDaemonErrorDomain 8 (password failed to validate) result := NewRecoveryLockResult(&mdm.CommandResults{ @@ -495,7 +537,11 @@ func TestSetRecoveryLockResultsHandler(t *testing.T) { return nil } - handler := NewSetRecoveryLockResultsHandler(ds, logger) + newActivityFn := func(_ context.Context, _ *fleet.User, _ fleet.ActivityDetails) error { + return nil + } + + handler := NewSetRecoveryLockResultsHandler(ds, logger, newActivityFn) // Test a generic transient error (not password mismatch) result := NewRecoveryLockResult(&mdm.CommandResults{ @@ -530,7 +576,11 @@ func TestSetRecoveryLockResultsHandler(t *testing.T) { return nil } - handler := NewSetRecoveryLockResultsHandler(ds, logger) + newActivityFn := func(_ context.Context, _ *fleet.User, _ fleet.ActivityDetails) error { + return nil + } + + handler := NewSetRecoveryLockResultsHandler(ds, logger, newActivityFn) // CommandFormatError is terminal - command is malformed and will never succeed result := NewRecoveryLockResult(&mdm.CommandResults{ diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index a5532037cb..c25996b88f 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -2961,7 +2961,7 @@ func (s *integrationMDMTestSuite) TestAppConfigMDMRecoveryLockPassword() { "mdm": { "enable_recovery_lock_password": true } }`), http.StatusOK, &acResp) assert.True(t, acResp.MDM.EnableRecoveryLockPassword.Value) - enabledActID := s.lastActivityMatches(fleet.ActivityTypeEnabledRecoveryLockPassword{}.ActivityName(), + enabledActID := s.lastActivityMatches(fleet.ActivityTypeEnabledRecoveryLockPasswords{}.ActivityName(), `{"team_id": null, "team_name": null, "fleet_id": null, "fleet_name": null}`, 0) // check that it's returned by GET /config @@ -2976,7 +2976,7 @@ func (s *integrationMDMTestSuite) TestAppConfigMDMRecoveryLockPassword() { }`), http.StatusOK, &acResp) assert.True(t, acResp.MDM.EnableRecoveryLockPassword.Value) // verify no new recovery lock password activity was created - s.lastActivityMatches(fleet.ActivityTypeEnabledRecoveryLockPassword{}.ActivityName(), + s.lastActivityMatches(fleet.ActivityTypeEnabledRecoveryLockPasswords{}.ActivityName(), ``, enabledActID) // patch with same value should not create new activity @@ -2985,7 +2985,7 @@ func (s *integrationMDMTestSuite) TestAppConfigMDMRecoveryLockPassword() { "mdm": { "enable_recovery_lock_password": true } }`), http.StatusOK, &acResp) assert.True(t, acResp.MDM.EnableRecoveryLockPassword.Value) - s.lastActivityMatches(fleet.ActivityTypeEnabledRecoveryLockPassword{}.ActivityName(), + s.lastActivityMatches(fleet.ActivityTypeEnabledRecoveryLockPasswords{}.ActivityName(), ``, enabledActID) // disable recovery lock password @@ -2994,7 +2994,7 @@ func (s *integrationMDMTestSuite) TestAppConfigMDMRecoveryLockPassword() { "mdm": { "enable_recovery_lock_password": false } }`), http.StatusOK, &acResp) assert.False(t, acResp.MDM.EnableRecoveryLockPassword.Value) - s.lastActivityMatches(fleet.ActivityTypeDisabledRecoveryLockPassword{}.ActivityName(), + s.lastActivityMatches(fleet.ActivityTypeDisabledRecoveryLockPasswords{}.ActivityName(), `{"team_id": null, "team_name": null, "fleet_id": null, "fleet_name": null}`, 0) // check that it's returned by GET /config @@ -3372,7 +3372,7 @@ func (s *integrationMDMTestSuite) TestTeamsMDMRecoveryLockPassword() { }, }, http.StatusOK, &modResp) require.True(t, modResp.Team.Config.MDM.EnableRecoveryLockPassword) - s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledRecoveryLockPassword{}.ActivityName(), + s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledRecoveryLockPasswords{}.ActivityName(), fmt.Sprintf(`{"team_id": %d, "team_name": %q, "fleet_id": %d, "fleet_name": %q}`, team.ID, teamName, team.ID, teamName), 0) // check it's returned by GET @@ -3381,7 +3381,7 @@ func (s *integrationMDMTestSuite) TestTeamsMDMRecoveryLockPassword() { require.True(t, teamResp.Team.Config.MDM.EnableRecoveryLockPassword) // patch with same value should not create new activity - lastActID := s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledRecoveryLockPassword{}.ActivityName(), + lastActID := s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledRecoveryLockPasswords{}.ActivityName(), ``, 0) modResp = teamResponse{} s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{ @@ -3390,7 +3390,7 @@ func (s *integrationMDMTestSuite) TestTeamsMDMRecoveryLockPassword() { }, }, http.StatusOK, &modResp) require.True(t, modResp.Team.Config.MDM.EnableRecoveryLockPassword) - s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledRecoveryLockPassword{}.ActivityName(), + s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledRecoveryLockPasswords{}.ActivityName(), ``, lastActID) // disable recovery lock password @@ -3401,7 +3401,7 @@ func (s *integrationMDMTestSuite) TestTeamsMDMRecoveryLockPassword() { }, }, http.StatusOK, &modResp) require.False(t, modResp.Team.Config.MDM.EnableRecoveryLockPassword) - s.lastActivityOfTypeMatches(fleet.ActivityTypeDisabledRecoveryLockPassword{}.ActivityName(), + s.lastActivityOfTypeMatches(fleet.ActivityTypeDisabledRecoveryLockPasswords{}.ActivityName(), fmt.Sprintf(`{"team_id": %d, "team_name": %q, "fleet_id": %d, "fleet_name": %q}`, team.ID, teamName, team.ID, teamName), 0) // check it's returned by GET diff --git a/server/service/testing_utils.go b/server/service/testing_utils.go index ed20281ed3..ef97f40f9a 100644 --- a/server/service/testing_utils.go +++ b/server/service/testing_utils.go @@ -529,7 +529,7 @@ func RunServerForTestsWithServiceWithDS(t *testing.T, ctx context.Context, ds fl checkInAndCommand := NewMDMAppleCheckinAndCommandService(ds, commander, vppInstaller, opts[0].License.IsPremium(), logger, redis_key_value.New(redisPool), svc.NewActivity) checkInAndCommand.RegisterResultsHandler("InstalledApplicationList", NewInstalledApplicationListResultsHandler(ds, commander, logger, cfg.Server.VPPVerifyTimeout, cfg.Server.VPPVerifyRequestDelay, svc.NewActivity)) checkInAndCommand.RegisterResultsHandler(fleet.DeviceLocationCmdName, NewDeviceLocationResultsHandler(ds, commander, logger)) - checkInAndCommand.RegisterResultsHandler(fleet.SetRecoveryLockCmdName, NewSetRecoveryLockResultsHandler(ds, logger)) + checkInAndCommand.RegisterResultsHandler(fleet.SetRecoveryLockCmdName, NewSetRecoveryLockResultsHandler(ds, logger, svc.NewActivity)) err := RegisterAppleMDMProtocolServices( rootMux, cfg.MDM,