Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions docs/Contributing/reference/audit-logs.md
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,36 @@ Generated when a user disables GitOps mode.

This activity does not contain any detail fields.

## enabled_gitops_exception

Generated when a user enables a GitOps exception.

This activity contains the following fields:
- "exception": Name of the exception that was enabled. One of `"labels"`, `"software"`, `"secrets"`.

#### Example

```json
{
"exception": "labels"
}
```

## disabled_gitops_exception

Generated when a user disables a GitOps exception.

This activity contains the following fields:
- "exception": Name of the exception that was disabled. One of `"labels"`, `"software"`, `"secrets"`.

#### Example

```json
{
"exception": "software"
}
```

## added_bootstrap_package

Generated when a user adds a new bootstrap package to a team (or no team).
Expand Down
5 changes: 5 additions & 0 deletions frontend/interfaces/activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ export enum ActivityType {
DisabledWindowsMdm = "disabled_windows_mdm",
EnabledGitOpsMode = "enabled_gitops_mode",
DisabledGitOpsMode = "disabled_gitops_mode",
EnabledGitOpsException = "enabled_gitops_exception",
DisabledGitOpsException = "disabled_gitops_exception",
EnabledWindowsMdmMigration = "enabled_windows_mdm_migration",
DisabledWindowsMdmMigration = "disabled_windows_mdm_migration",
RanScript = "ran_script",
Expand Down Expand Up @@ -302,6 +304,7 @@ export interface IActivityDetails {
certificate_name?: string;
certificate_template_id?: number;
detail?: string;
exception?: string;
}

// maps activity types to their corresponding label to use when filtering activites via the dropdown
Expand Down Expand Up @@ -362,6 +365,7 @@ export const ACTIVITY_TYPE_TO_FILTER_LABEL: Record<ActivityType, string> = {
disabled_android_mdm: "Turned off Android MDM",
disabled_conditional_access_automations:
"Disabled conditional access automations",
disabled_gitops_exception: "Disabled GitOps exception",
disabled_gitops_mode: "Disabled GitOps mode",
disabled_macos_disk_encryption: "Turned off disk encryption",
disabled_macos_setup_end_user_auth:
Expand Down Expand Up @@ -393,6 +397,7 @@ export const ACTIVITY_TYPE_TO_FILTER_LABEL: Record<ActivityType, string> = {
enabled_android_mdm: "Turned on Android MDM",
enabled_conditional_access_automations:
"Enabled conditional access automations",
enabled_gitops_exception: "Enabled GitOps exception",
enabled_gitops_mode: "Enabled GitOps mode",
enabled_macos_disk_encryption: "Turned on disk encryption",
enabled_macos_setup_end_user_auth:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1817,4 +1817,26 @@ describe("Activity Feed", () => {
expect(screen.getByText(/Lions/i)).toBeInTheDocument();
expect(screen.getByText(/fleet/i)).toBeInTheDocument();
});

it("renders an enabled_gitops_exception activity", () => {
const activity = createMockActivity({
type: ActivityType.EnabledGitOpsException,
details: { exception: "labels" },
});
render(<GlobalActivityItem activity={activity} isPremiumTier />);
expect(
screen.getByText("enabled the labels exception for GitOps.")
).toBeInTheDocument();
});

it("renders a disabled_gitops_exception activity", () => {
const activity = createMockActivity({
type: ActivityType.DisabledGitOpsException,
details: { exception: "software" },
});
render(<GlobalActivityItem activity={activity} isPremiumTier />);
expect(
screen.getByText("disabled the software exception for GitOps.")
).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -958,6 +958,14 @@ const TAGGED_TEMPLATES = {
},
enabledGitOpsMode: () => "enabled GitOps mode in the UI.",
disabledGitOpsMode: () => "disabled GitOps mode in the UI.",
enabledGitOpsException: (activity: IActivity) => {
const exception = activity.details?.exception ?? "";
return `enabled the ${exception} exception for GitOps.`;
},
disabledGitOpsException: (activity: IActivity) => {
const exception = activity.details?.exception ?? "";
return `disabled the ${exception} exception for GitOps.`;
},
enabledWindowsMdmMigration: () => {
return (
<>
Expand Down Expand Up @@ -2097,6 +2105,12 @@ const getDetail = (activity: IActivity, isPremiumTier: boolean) => {
case ActivityType.DisabledGitOpsMode: {
return TAGGED_TEMPLATES.disabledGitOpsMode();
}
case ActivityType.EnabledGitOpsException: {
return TAGGED_TEMPLATES.enabledGitOpsException(activity);
}
case ActivityType.DisabledGitOpsException: {
return TAGGED_TEMPLATES.disabledGitOpsException(activity);
}
case ActivityType.EnabledWindowsMdmMigration: {
return TAGGED_TEMPLATES.enabledWindowsMdmMigration();
}
Expand Down
18 changes: 18 additions & 0 deletions server/fleet/activities.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ var ActivityDetailsList = []ActivityDetails{

ActivityTypeEnabledGitOpsMode{},
ActivityTypeDisabledGitOpsMode{},
ActivityTypeEnabledGitOpsException{},
ActivityTypeDisabledGitOpsException{},

ActivityTypeAddedBootstrapPackage{},
ActivityTypeDeletedBootstrapPackage{},
Expand Down Expand Up @@ -862,6 +864,22 @@ func (a ActivityTypeDisabledGitOpsMode) ActivityName() string {
return "disabled_gitops_mode"
}

type ActivityTypeEnabledGitOpsException struct {
Exception string `json:"exception"`
}

func (a ActivityTypeEnabledGitOpsException) ActivityName() string {
return "enabled_gitops_exception"
}

type ActivityTypeDisabledGitOpsException struct {
Exception string `json:"exception"`
}

func (a ActivityTypeDisabledGitOpsException) ActivityName() string {
return "disabled_gitops_exception"
}

type ActivityTypeAddedBootstrapPackage struct {
BootstrapPackageName string `json:"bootstrap_package_name"`
TeamID *uint `json:"team_id" renameto:"fleet_id"`
Expand Down
26 changes: 26 additions & 0 deletions server/service/appconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,32 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle
return nil, err
}

oldExceptions := oldAppConfig.GitOpsConfig.Exceptions
newExceptions := appConfig.GitOpsConfig.Exceptions
exceptionChanges := []struct {
name string
oldEnabled bool
newEnabled bool
}{
{"labels", oldExceptions.Labels, newExceptions.Labels},
{"software", oldExceptions.Software, newExceptions.Software},
{"secrets", oldExceptions.Secrets, newExceptions.Secrets},
}
for _, c := range exceptionChanges {
if c.oldEnabled == c.newEnabled {
continue
}
var act fleet.ActivityDetails
if c.newEnabled {
act = fleet.ActivityTypeEnabledGitOpsException{Exception: c.name}
} else {
act = fleet.ActivityTypeDisabledGitOpsException{Exception: c.name}
}
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil {
return nil, ctxerr.Wrapf(ctx, err, "create activity %s", act.ActivityName())
}
}
Comment thread
sgress454 marked this conversation as resolved.
Comment thread
sgress454 marked this conversation as resolved.

addedEntraTenantIDs := make([]string, 0)
removedEntraTenantIDs := make([]string, 0)
oldTenantIDSet := make(map[string]struct{})
Expand Down
133 changes: 133 additions & 0 deletions server/service/appconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"testing"

"github.com/fleetdm/fleet/v4/pkg/optjson"
activity_api "github.com/fleetdm/fleet/v4/server/activity/api"
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/fleet"
Expand Down Expand Up @@ -2014,3 +2015,135 @@ func TestModifyAppConfigGoogleCalendarAPIKey(t *testing.T) {
require.True(t, updatedAppConfig.Integrations.GoogleCalendar[0].ApiKey.IsMasked())
})
}

func TestModifyAppConfigGitOpsExceptionActivities(t *testing.T) {
admin := &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}

type exceptionActivity struct {
name string // activity name: "enabled_gitops_exception" or "disabled_gitops_exception"
exception string
}

testCases := []struct {
name string
initial fleet.GitOpsExceptions
patch string
expectFired []exceptionActivity
}{
{
name: "no change fires no activity",
initial: fleet.GitOpsExceptions{Labels: true, Software: false, Secrets: true},
patch: `{"gitops": {"exceptions": {"labels": true, "software": false, "secrets": true}}}`,
expectFired: nil,
},
{
name: "single field flip fires one activity",
initial: fleet.GitOpsExceptions{Labels: false, Software: false, Secrets: true},
patch: `{"gitops": {"exceptions": {"labels": true, "software": false, "secrets": true}}}`,
expectFired: []exceptionActivity{
{name: "enabled_gitops_exception", exception: "labels"},
},
},
{
name: "multiple field flips fire one activity each, in order",
initial: fleet.GitOpsExceptions{Labels: false, Software: true, Secrets: false},
patch: `{"gitops": {"exceptions": {"labels": true, "software": false, "secrets": true}}}`,
expectFired: []exceptionActivity{
{name: "enabled_gitops_exception", exception: "labels"},
{name: "disabled_gitops_exception", exception: "software"},
{name: "enabled_gitops_exception", exception: "secrets"},
},
},
}

for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
ds := new(mock.Store)
opts := &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}}
svc, ctx := newTestService(t, ds, nil, nil, opts)
ctx = viewer.NewContext(ctx, viewer.Viewer{User: admin})

dsAppConfig := &fleet.AppConfig{
OrgInfo: fleet.OrgInfo{OrgName: "Test"},
ServerSettings: fleet.ServerSettings{ServerURL: "https://example.org"},
GitOpsConfig: fleet.GitOpsConfig{
GitopsModeEnabled: true,
RepositoryURL: "https://example.com/repo",
Exceptions: tt.initial,
},
}

ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return dsAppConfig, nil
}
ds.SaveAppConfigFunc = func(ctx context.Context, conf *fleet.AppConfig) error {
*dsAppConfig = *conf
return nil
}
ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error { return nil }
ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
return []*fleet.VPPTokenDB{}, nil
}
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
return []*fleet.ABMToken{}, nil
}

var fired []exceptionActivity
opts.ActivityMock.NewActivityFunc = func(_ context.Context, _ *activity_api.User, act activity_api.ActivityDetails) error {
switch ex := act.(type) {
case fleet.ActivityTypeEnabledGitOpsException:
fired = append(fired, exceptionActivity{name: act.ActivityName(), exception: ex.Exception})
case fleet.ActivityTypeDisabledGitOpsException:
fired = append(fired, exceptionActivity{name: act.ActivityName(), exception: ex.Exception})
}
return nil
}

_, err := svc.ModifyAppConfig(ctx, []byte(tt.patch), fleet.ApplySpecOptions{})
require.NoError(t, err)
require.Equal(t, tt.expectFired, fired)
})
}

t.Run("no activity is emitted when SaveAppConfig fails", func(t *testing.T) {
ds := new(mock.Store)
opts := &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}}
svc, ctx := newTestService(t, ds, nil, nil, opts)
ctx = viewer.NewContext(ctx, viewer.Viewer{User: admin})

dsAppConfig := &fleet.AppConfig{
OrgInfo: fleet.OrgInfo{OrgName: "Test"},
ServerSettings: fleet.ServerSettings{ServerURL: "https://example.org"},
GitOpsConfig: fleet.GitOpsConfig{
GitopsModeEnabled: true,
RepositoryURL: "https://example.com/repo",
Exceptions: fleet.GitOpsExceptions{Labels: false, Software: false, Secrets: false},
},
}

ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { return dsAppConfig, nil }
ds.SaveAppConfigFunc = func(ctx context.Context, conf *fleet.AppConfig) error {
return errors.New("save failed")
}
ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error { return nil }
ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) { return []*fleet.VPPTokenDB{}, nil }
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { return []*fleet.ABMToken{}, nil }

var fired []exceptionActivity
opts.ActivityMock.NewActivityFunc = func(_ context.Context, _ *activity_api.User, act activity_api.ActivityDetails) error {
switch ex := act.(type) {
case fleet.ActivityTypeEnabledGitOpsException:
fired = append(fired, exceptionActivity{name: act.ActivityName(), exception: ex.Exception})
case fleet.ActivityTypeDisabledGitOpsException:
fired = append(fired, exceptionActivity{name: act.ActivityName(), exception: ex.Exception})
}
return nil
}

_, err := svc.ModifyAppConfig(ctx,
[]byte(`{"gitops": {"exceptions": {"labels": true, "software": true, "secrets": true}}}`),
fleet.ApplySpecOptions{})
require.Error(t, err)
require.Empty(t, fired, "no exception activity should be emitted when SaveAppConfig fails")
})
}
Loading
Loading