Add "edited macos profiles" activity when applying custom settings via fleetctl (#9862)

#9587 and #9639
This commit is contained in:
Martin Angers 2023-02-16 11:53:26 -05:00 committed by GitHub
parent 2e6786f8c4
commit 4a1f3988f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 196 additions and 19 deletions

View file

@ -0,0 +1 @@
* Added "edited macos profiles" activity when updating a team's (or no team's) custom macOS settings via `fleetctl apply`.

View file

@ -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"
}
```
<meta name="pageOrderInSection" value="1400">

View file

@ -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

View file

@ -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
}

View file

@ -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"
}`
}

View file

@ -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 {

View file

@ -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
}
////////////////////////////////////////////////////////////////////////////////

View file

@ -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

View file

@ -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 {