Recovery lock password activities (#41529)

This commit is contained in:
Tim Lee
2026-03-17 08:48:23 -06:00
committed by GitHub
parent 2b75dafaa9
commit 012147ab85
8 changed files with 151 additions and 32 deletions
+1 -1
View File
@@ -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 {
+27 -4
View File
@@ -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 == "" &&
+26 -8
View File
@@ -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{}
+2 -2
View File
@@ -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")
+28
View File
@@ -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,
)
}
+58 -8
View File
@@ -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{
+8 -8
View File
@@ -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
+1 -1
View File
@@ -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,