diff --git a/changes/issue-9587-record-activity b/changes/issue-9587-record-activity new file mode 100644 index 0000000000..b01cbf872d --- /dev/null +++ b/changes/issue-9587-record-activity @@ -0,0 +1 @@ +* Added "edited macos profiles" activity when updating a team's (or no team's) custom macOS settings via `fleetctl apply`. diff --git a/docs/Using-Fleet/Audit-Activities.md b/docs/Using-Fleet/Audit-Activities.md index 0ea6eb5f9d..61381b8f5c 100644 --- a/docs/Using-Fleet/Audit-Activities.md +++ b/docs/Using-Fleet/Audit-Activities.md @@ -600,6 +600,65 @@ This activity contains the following fields: } ``` +### Type `created_macos_profile` + +Generated when a user adds a new macOS profile to a team (or no team). + +This activity contains the following fields: +- "profile_name": Name of the profile. +- "profile_identifier": Identifier of the profile. +- "team_id": The ID of the team that the profile applies to, null if it applies to devices that are not in a team. +- "team_name": The name of the team that the profile applies to, null if it applies to devices that are not in a team. + +#### Example + +```json +{ + "profile_name": "Custom settings 1", + "profile_identifier": "com.my.profile", + "team_id": 123, + "team_name": "Workstations" +} +``` + +### Type `deleted_macos_profile` + +Generated when a user deletes a macOS profile from a team (or no team). + +This activity contains the following fields: +- "profile_name": Name of the deleted profile. +- "profile_identifier": Identifier of deleted the profile. +- "team_id": The ID of the team that the profile applied to, null if it applied to devices that are not in a team. +- "team_name": The name of the team that the profile applied to, null if it applied to devices that are not in a team. + +#### Example + +```json +{ + "profile_name": "Custom settings 1", + "profile_identifier": "com.my.profile", + "team_id": 123, + "team_name": "Workstations" +} +``` + +### Type `edited_macos_profile` + +Generated when a user edits the macOS profiles of a team (or no team) via the fleetctl CLI. + +This activity contains the following fields: +- "team_id": The ID of the team that the profiles apply to, null if they apply to devices that are not in a team. +- "team_name": The name of the team that the profiles apply to, null if they apply to devices that are not in a team. + +#### Example + +```json +{ + "team_id": 123, + "team_name": "Workstations" +} +``` + \ No newline at end of file diff --git a/ee/server/service/service.go b/ee/server/service/service.go index 0baea29291..72c284617e 100644 --- a/ee/server/service/service.go +++ b/ee/server/service/service.go @@ -50,8 +50,8 @@ func NewService( // Override methods that can't be easily overriden via // embedding. svc.SetEnterpriseOverrides(fleet.EnterpriseOverrides{ - HostFeatures: eeservice.HostFeatures, - TeamByName: eeservice.teamByName, + HostFeatures: eeservice.HostFeatures, + TeamByIDOrName: eeservice.teamByIDOrName, }) return eeservice, nil diff --git a/ee/server/service/teams.go b/ee/server/service/teams.go index ca200a0c4e..6c64b77126 100644 --- a/ee/server/service/teams.go +++ b/ee/server/service/teams.go @@ -415,21 +415,33 @@ func (svc *Service) ModifyTeamEnrollSecrets(ctx context.Context, teamID uint, se return newSecrets, nil } -func (svc *Service) teamByName(ctx context.Context, name string) (*fleet.Team, error) { +func (svc *Service) teamByIDOrName(ctx context.Context, id *uint, name *string) (*fleet.Team, error) { if err := svc.authz.Authorize(ctx, &fleet.Team{}, fleet.ActionRead); err != nil { return nil, err } - tm, err := svc.ds.TeamByName(ctx, name) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - // this should really be handled in TeamByName so that it returns a - // notFound error as is usually the case for this scenario, but - // changing it causes a number of test failures that indicates this - // might be tricky and even maybe a breaking change in some places. For - // now, handling it here. - return nil, notFoundError{} + + var ( + tm *fleet.Team + err error + ) + if id != nil { + tm, err = svc.ds.Team(ctx, *id) + if err != nil { + return nil, err + } + } else if name != nil { + tm, err = svc.ds.TeamByName(ctx, *name) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + // this should really be handled in TeamByName so that it returns a + // notFound error as is usually the case for this scenario, but + // changing it causes a number of test failures that indicates this + // might be tricky and even maybe a breaking change in some places. For + // now, handling it here. + return nil, notFoundError{} + } + return nil, err } - return nil, err } return tm, nil } diff --git a/server/fleet/activities.go b/server/fleet/activities.go index 7aaf71a5bd..76fa55b200 100644 --- a/server/fleet/activities.go +++ b/server/fleet/activities.go @@ -49,6 +49,10 @@ var ActivityDetailsList = []ActivityDetails{ ActivityTypeEditedMacOSMinVersion{}, ActivityTypeReadHostDiskEncryptionKey{}, + + ActivityTypeCreatedMacosProfile{}, + ActivityTypeDeletedMacosProfile{}, + ActivityTypeEditedMacosProfile{}, } type ActivityDetails interface { @@ -729,3 +733,72 @@ func (a ActivityTypeReadHostDiskEncryptionKey) Documentation() (activity string, "host_display_name": "Anna's MacBook Pro", }` } + +type ActivityTypeCreatedMacosProfile struct { + ProfileName string `json:"profile_name"` + ProfileIdentifier string `json:"profile_identifier"` + TeamID *uint `json:"team_id"` + TeamName *string `json:"team_name"` +} + +func (a ActivityTypeCreatedMacosProfile) ActivityName() string { + return "created_macos_profile" +} + +func (a ActivityTypeCreatedMacosProfile) Documentation() (activity, details, detailsExample string) { + return `Generated when a user adds a new macOS profile to a team (or no team).`, + `This activity contains the following fields: +- "profile_name": Name of the profile. +- "profile_identifier": Identifier of the profile. +- "team_id": The ID of the team that the profile applies to, null if it applies to devices that are not in a team. +- "team_name": The name of the team that the profile applies to, null if it applies to devices that are not in a team.`, `{ + "profile_name": "Custom settings 1", + "profile_identifier": "com.my.profile", + "team_id": 123, + "team_name": "Workstations" +}` +} + +type ActivityTypeDeletedMacosProfile struct { + ProfileName string `json:"profile_name"` + ProfileIdentifier string `json:"profile_identifier"` + TeamID *uint `json:"team_id"` + TeamName *string `json:"team_name"` +} + +func (a ActivityTypeDeletedMacosProfile) ActivityName() string { + return "deleted_macos_profile" +} + +func (a ActivityTypeDeletedMacosProfile) Documentation() (activity, details, detailsExample string) { + return `Generated when a user deletes a macOS profile from a team (or no team).`, + `This activity contains the following fields: +- "profile_name": Name of the deleted profile. +- "profile_identifier": Identifier of deleted the profile. +- "team_id": The ID of the team that the profile applied to, null if it applied to devices that are not in a team. +- "team_name": The name of the team that the profile applied to, null if it applied to devices that are not in a team.`, `{ + "profile_name": "Custom settings 1", + "profile_identifier": "com.my.profile", + "team_id": 123, + "team_name": "Workstations" +}` +} + +type ActivityTypeEditedMacosProfile struct { + TeamID *uint `json:"team_id"` + TeamName *string `json:"team_name"` +} + +func (a ActivityTypeEditedMacosProfile) ActivityName() string { + return "edited_macos_profile" +} + +func (a ActivityTypeEditedMacosProfile) Documentation() (activity, details, detailsExample string) { + return `Generated when a user edits the macOS profiles of a team (or no team) via the fleetctl CLI.`, + `This activity contains the following fields: +- "team_id": The ID of the team that the profiles apply to, null if they apply to devices that are not in a team. +- "team_name": The name of the team that the profiles apply to, null if they apply to devices that are not in a team.`, `{ + "team_id": 123, + "team_name": "Workstations" +}` +} diff --git a/server/fleet/service.go b/server/fleet/service.go index cec886cb10..b6fd2adbb6 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -15,8 +15,8 @@ import ( // // TODO: find if there's a better way to accomplish this and standardize. type EnterpriseOverrides struct { - HostFeatures func(context context.Context, host *Host) (*Features, error) - TeamByName func(ctx context.Context, name string) (*Team, error) + HostFeatures func(context context.Context, host *Host) (*Features, error) + TeamByIDOrName func(ctx context.Context, id *uint, name *string) (*Team, error) } type OsqueryService interface { diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index fd5c50ba86..d865815035 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -1157,12 +1157,18 @@ func (svc *Service) BatchSetMDMAppleProfiles(ctx context.Context, tmID *uint, tm } // if the team name is provided, load the corresponding team to get its id. - if tmName != nil { - tm, err := svc.EnterpriseOverrides.TeamByName(ctx, *tmName) + // vice-versa, if the id is provided, load it to get the name (required for + // the activity). + if tmName != nil || tmID != nil { + tm, err := svc.EnterpriseOverrides.TeamByIDOrName(ctx, tmID, tmName) if err != nil { return err } - tmID = &tm.ID + if tmID == nil { + tmID = &tm.ID + } else { + tmName = &tm.Name + } } if err := svc.authz.Authorize(ctx, &fleet.MDMAppleConfigProfile{TeamID: tmID}, fleet.ActionWrite); err != nil { @@ -1201,7 +1207,17 @@ func (svc *Service) BatchSetMDMAppleProfiles(ctx context.Context, tmID *uint, tm if dryRun { return nil } - return svc.ds.BatchSetMDMAppleProfiles(ctx, tmID, profs) + if err := svc.ds.BatchSetMDMAppleProfiles(ctx, tmID, profs); err != nil { + return err + } + + if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeEditedMacosProfile{ + TeamID: tmID, + TeamName: tmName, + }); err != nil { + return ctxerr.Wrap(ctx, err, "logging activity for edited macos profile") + } + return nil } //////////////////////////////////////////////////////////////////////////////// diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index 1b82cfe418..feea1a13b3 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -502,9 +502,15 @@ func TestMDMBatchSetAppleProfiles(t *testing.T) { ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) { return &fleet.Team{ID: 1, Name: name}, nil } + ds.TeamFunc = func(ctx context.Context, id uint) (*fleet.Team, error) { + return &fleet.Team{ID: id, Name: "team"}, nil + } ds.BatchSetMDMAppleProfilesFunc = func(ctx context.Context, teamID *uint, profiles []*fleet.MDMAppleConfigProfile) error { return nil } + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + return nil + } testCases := []struct { name string diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index e2acad8525..a6f928d964 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -859,6 +859,11 @@ func (s *integrationMDMTestSuite) TestBatchSetMDMAppleProfiles() { // apply an empty set to no-team s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: nil}, http.StatusNoContent) + s.lastActivityMatches( + fleet.ActivityTypeEditedMacosProfile{}.ActivityName(), + `{"team_id": null, "team_name": null}`, + 0, + ) // apply to both team id and name s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: nil}, @@ -878,6 +883,11 @@ func (s *integrationMDMTestSuite) TestBatchSetMDMAppleProfiles() { s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: [][]byte{ mobileconfigForTest("N1", "I1"), }}, http.StatusNoContent, "team_id", strconv.Itoa(int(tm.ID))) + s.lastActivityMatches( + fleet.ActivityTypeEditedMacosProfile{}.ActivityName(), + fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, tm.ID, tm.Name), + 0, + ) } type device struct {