mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Move NewActivity to activity bounded context (#39521)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #38536 This PR moves all logic to create new activities to activity bounded context. The old service and ActivityModule methods are not facades that route to the new activity bounded context. The facades will be removed in a subsequent PR. # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added webhook support for activity events with configurable endpoint and enable/disable settings. * Enhanced automation-initiated activity creation without requiring a user context. * Improved activity service architecture with centralized creation and management. * **Improvements** * Refactored activity creation to use a dedicated service layer for better separation of concerns. * Added support for host-specific and automation-originated activities. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
7817d93da1
commit
913a5904c8
92 changed files with 1684 additions and 1287 deletions
1
changes/38536-new-activity-bc
Normal file
1
changes/38536-new-activity-bc
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Refactored NewActivity functionality by moving it to the new activity bounded context.
|
||||
|
|
@ -1896,6 +1896,7 @@ func newAndroidMDMDeviceReconcilerSchedule(
|
|||
ds fleet.Datastore,
|
||||
logger *logging.Logger,
|
||||
licenseKey string,
|
||||
newActivityFn fleet.NewActivityFunc,
|
||||
) (*schedule.Schedule, error) {
|
||||
const (
|
||||
name = string(fleet.CronMDMAndroidDeviceReconciler)
|
||||
|
|
@ -1907,7 +1908,7 @@ func newAndroidMDMDeviceReconcilerSchedule(
|
|||
ctx, name, instanceID, defaultInterval, ds, ds,
|
||||
schedule.WithLogger(logger),
|
||||
schedule.WithJob("reconcile_android_devices", func(ctx context.Context) error {
|
||||
return android_svc.ReconcileAndroidDevices(ctx, ds, logger.SlogLogger(), licenseKey)
|
||||
return android_svc.ReconcileAndroidDevices(ctx, ds, logger.SlogLogger(), licenseKey, newActivityFn)
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -875,7 +875,7 @@ the way that the Fleet server works.
|
|||
digiCertService := digicert.NewService(digicert.WithLogger(logger))
|
||||
ctx = ctxerr.NewContext(ctx, eh)
|
||||
|
||||
activitiesModule := activities.NewActivityModule(ds, logger)
|
||||
activitiesModule := activities.NewActivityModule()
|
||||
config.MDM.AndroidAgent.Validate(initFatal)
|
||||
androidSvc, err := android_service.NewService(
|
||||
ctx,
|
||||
|
|
@ -1040,6 +1040,9 @@ the way that the Fleet server works.
|
|||
|
||||
// Bootstrap activity bounded context (needed for cron schedules and HTTP routes)
|
||||
activitySvc, activityRoutes := createActivityBoundedContext(svc, dbConns, logger.SlogLogger())
|
||||
// Inject the activity bounded context into the main service and activity module
|
||||
svc.SetActivityService(activitySvc)
|
||||
activitiesModule.SetService(activitySvc)
|
||||
|
||||
// Perform a cleanup of cron_stats outside of the cronSchedules because the
|
||||
// schedule package uses cron_stats entries to decide whether a schedule will
|
||||
|
|
@ -1232,6 +1235,7 @@ the way that the Fleet server works.
|
|||
ds,
|
||||
logger,
|
||||
config.License.Key,
|
||||
svc.NewActivity,
|
||||
)
|
||||
}); err != nil {
|
||||
initFatal(err, "failed to register mdm_android_device_reconciler schedule")
|
||||
|
|
@ -1462,9 +1466,10 @@ the way that the Fleet server works.
|
|||
license.IsPremium(),
|
||||
logger,
|
||||
redis_key_value.New(redisPool),
|
||||
svc.NewActivity,
|
||||
)
|
||||
|
||||
mdmCheckinAndCommandService.RegisterResultsHandler("InstalledApplicationList", service.NewInstalledApplicationListResultsHandler(ds, commander, logger, config.Server.VPPVerifyTimeout, config.Server.VPPVerifyRequestDelay))
|
||||
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))
|
||||
|
||||
hasSCEPChallenge, err := checkMDMAssets([]fleet.MDMAssetName{fleet.MDMAssetSCEPChallenge})
|
||||
|
|
@ -1808,6 +1813,7 @@ func createActivityBoundedContext(svc fleet.Service, dbConns *common_mysql.DBCon
|
|||
dbConns,
|
||||
activityAuthorizer,
|
||||
activityACLAdapter,
|
||||
server.PostJSONWithTimeout,
|
||||
logger,
|
||||
)
|
||||
// Create auth middleware for activity bounded context
|
||||
|
|
|
|||
|
|
@ -271,12 +271,6 @@ func TestApplyTeamSpecs(t *testing.T) {
|
|||
return nil
|
||||
}
|
||||
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
ds.LabelIDsByNameFunc = func(ctx context.Context, names []string, filter fleet.TeamFilter) (map[string]uint, error) {
|
||||
require.Len(t, names, 1)
|
||||
switch names[0] {
|
||||
|
|
@ -698,12 +692,6 @@ func TestApplyAppConfig(t *testing.T) {
|
|||
return nil
|
||||
}
|
||||
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
ds.UserByEmailFunc = func(ctx context.Context, email string) (*fleet.User, error) {
|
||||
if email == "admin1@example.com" {
|
||||
return userRoleSpecList[0], nil
|
||||
|
|
@ -920,12 +908,6 @@ func TestApplyAppConfigDryRunIssue(t *testing.T) {
|
|||
return userRoleSpecList[1], nil
|
||||
}
|
||||
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
currentAppConfig := &fleet.AppConfig{
|
||||
OrgInfo: fleet.OrgInfo{OrgName: "Fleet"}, ServerSettings: fleet.ServerSettings{ServerURL: "https://example.org"},
|
||||
}
|
||||
|
|
@ -1295,12 +1277,6 @@ func TestApplyPolicies(t *testing.T) {
|
|||
}
|
||||
return nil, errors.New("unexpected team name!")
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
name := writeTmpYml(t, policySpec)
|
||||
|
||||
assert.Equal(t, "[+] applied 3 policies\n", RunAppForTest(t, []string{"apply", "-f", name}))
|
||||
|
|
@ -1389,12 +1365,6 @@ func TestApplyAsGitOps(t *testing.T) {
|
|||
ds.UserByIDFunc = func(ctx context.Context, id uint) (*fleet.User, error) {
|
||||
return gitOps, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
currentAppConfig := &fleet.AppConfig{
|
||||
OrgInfo: fleet.OrgInfo{
|
||||
OrgName: "Fleet",
|
||||
|
|
@ -1982,12 +1952,6 @@ func TestApplyPacks(t *testing.T) {
|
|||
ds.ListPacksFunc = func(ctx context.Context, opt fleet.PackListOptions) ([]*fleet.Pack, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var appliedPacks []*fleet.PackSpec
|
||||
ds.ApplyPackSpecsFunc = func(ctx context.Context, specs []*fleet.PackSpec) error {
|
||||
appliedPacks = specs
|
||||
|
|
@ -2034,12 +1998,6 @@ func TestApplyQueries(t *testing.T) {
|
|||
appliedQueries = queries
|
||||
return nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
name := writeTmpYml(t, queriesSpec)
|
||||
|
||||
assert.Equal(t, "[+] applied 1 query\n", RunAppForTest(t, []string{"apply", "-f", name}))
|
||||
|
|
@ -2166,11 +2124,6 @@ func TestApplyMacosSetup(t *testing.T) {
|
|||
teamsByID := map[uint]*fleet.Team{
|
||||
tm1.ID: tm1,
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
|
||||
return job, nil
|
||||
}
|
||||
|
|
@ -3018,13 +2971,6 @@ func TestApplySpecs(t *testing.T) {
|
|||
return nil
|
||||
}
|
||||
|
||||
// activities
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// app config
|
||||
ds.ListUsersFunc = func(ctx context.Context, opt fleet.UserListOptions) ([]*fleet.User, error) {
|
||||
return userRoleSpecList, nil
|
||||
|
|
@ -4318,10 +4264,6 @@ func TestApplyWindowsUpdates(t *testing.T) {
|
|||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) (fleet.MDMProfilesUpdates, error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
t.Run("with values", func(t *testing.T) {
|
||||
// Reset call trackers
|
||||
setOrUpdateCalls = nil
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package fleetctl
|
|||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/cmd/fleetctl/fleetctl/testing_utils"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
|
|
@ -57,12 +56,6 @@ func TestDeletePack(t *testing.T) {
|
|||
Disabled: false,
|
||||
}, true, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
name := writeTmpYml(t, `---
|
||||
apiVersion: v1
|
||||
kind: pack
|
||||
|
|
@ -102,12 +95,6 @@ func TestDeleteQuery(t *testing.T) {
|
|||
ObserverCanRun: false,
|
||||
}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
name := writeTmpYml(t, `---
|
||||
apiVersion: v1
|
||||
kind: query
|
||||
|
|
|
|||
|
|
@ -2576,11 +2576,6 @@ func TestGetTeamsYAMLAndApply(t *testing.T) {
|
|||
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
|
||||
return nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
|
||||
if name == "team1" {
|
||||
return team1, nil
|
||||
|
|
|
|||
|
|
@ -102,11 +102,6 @@ func TestGitOpsBasicGlobalFree(t *testing.T) {
|
|||
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) ([]fleet.ScriptResponse, error) {
|
||||
return []fleet.ScriptResponse{}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.ListGlobalPoliciesFunc = func(ctx context.Context, opts fleet.ListOptions) ([]*fleet.Policy, error) { return nil, nil }
|
||||
ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) {
|
||||
return nil, 0, 0, nil, nil
|
||||
|
|
@ -288,11 +283,6 @@ func TestGitOpsBasicGlobalPremium(t *testing.T) {
|
|||
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) ([]fleet.ScriptResponse, error) {
|
||||
return []fleet.ScriptResponse{}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.ListGlobalPoliciesFunc = func(ctx context.Context, opts fleet.ListOptions) ([]*fleet.Policy, error) { return nil, nil }
|
||||
ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) {
|
||||
return nil, 0, 0, nil, nil
|
||||
|
|
@ -641,11 +631,6 @@ func TestGitOpsBasicTeam(t *testing.T) {
|
|||
ds.DeleteMDMWindowsConfigProfileByTeamAndNameFunc = func(ctx context.Context, teamID *uint, profileName string) error {
|
||||
return nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.ListTeamPoliciesFunc = func(
|
||||
ctx context.Context, teamID uint, opts fleet.ListOptions, iopts fleet.ListOptions,
|
||||
) (teamPolicies []*fleet.Policy, inheritedPolicies []*fleet.Policy, err error) {
|
||||
|
|
@ -893,11 +878,6 @@ func TestGitOpsFullGlobal(t *testing.T) {
|
|||
|
||||
return scriptResponses, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
var appliedMacProfiles []*fleet.MDMAppleConfigProfile
|
||||
var appliedWinProfiles []*fleet.MDMWindowsConfigProfile
|
||||
ds.BatchSetMDMProfilesFunc = func(
|
||||
|
|
@ -1195,11 +1175,6 @@ func TestGitOpsFullTeam(t *testing.T) {
|
|||
|
||||
return scriptResponses, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
var appliedMacProfiles []*fleet.MDMAppleConfigProfile
|
||||
var appliedWinProfiles []*fleet.MDMWindowsConfigProfile
|
||||
ds.BatchSetMDMProfilesFunc = func(
|
||||
|
|
@ -1666,11 +1641,6 @@ func TestGitOpsBasicGlobalAndTeam(t *testing.T) {
|
|||
// Mock DefaultTeamConfig functions for No Team webhook settings
|
||||
setupDefaultTeamConfigMocks(ds)
|
||||
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
|
||||
job.ID = 1
|
||||
return job, nil
|
||||
|
|
@ -2067,11 +2037,6 @@ func TestGitOpsBasicGlobalAndNoTeam(t *testing.T) {
|
|||
}
|
||||
testing_utils.AddLabelMocks(ds)
|
||||
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
|
||||
job.ID = 1
|
||||
return job, nil
|
||||
|
|
@ -4545,9 +4510,6 @@ func TestGitOpsWindowsUpdates(t *testing.T) {
|
|||
ds.ListTeamPoliciesFunc = func(ctx context.Context, teamID uint, opts fleet.ListOptions, iopts fleet.ListOptions) ([]*fleet.Policy, []*fleet.Policy, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration, androidProfiles []*fleet.MDMAndroidConfigProfile, vars []fleet.MDMProfileIdentifierFleetVariables) (fleet.MDMProfilesUpdates, error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
|
|
@ -4951,9 +4913,6 @@ func TestGitOpsAppStoreAppAutoUpdate(t *testing.T) {
|
|||
ds.ListTeamPoliciesFunc = func(ctx context.Context, teamID uint, opts fleet.ListOptions, iopts fleet.ListOptions) ([]*fleet.Policy, []*fleet.Policy, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration, androidProfiles []*fleet.MDMAndroidConfigProfile, vars []fleet.MDMProfileIdentifierFleetVariables) (fleet.MDMProfilesUpdates, error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
|
|
@ -5281,9 +5240,6 @@ func TestGitOpsAppleOSUpdates(t *testing.T) {
|
|||
ds.ListTeamPoliciesFunc = func(ctx context.Context, teamID uint, opts fleet.ListOptions, iopts fleet.ListOptions) ([]*fleet.Policy, []*fleet.Policy, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration, androidProfiles []*fleet.MDMAndroidConfigProfile, vars []fleet.MDMProfileIdentifierFleetVariables) (fleet.MDMProfilesUpdates, error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
|
|
@ -5641,9 +5597,6 @@ func TestGitOpsWindowsOSUpdates(t *testing.T) {
|
|||
ds.ListTeamPoliciesFunc = func(ctx context.Context, teamID uint, opts fleet.ListOptions, iopts fleet.ListOptions) ([]*fleet.Policy, []*fleet.Policy, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration, androidProfiles []*fleet.MDMAndroidConfigProfile, vars []fleet.MDMProfileIdentifierFleetVariables) (fleet.MDMProfilesUpdates, error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@ package fleetctl
|
|||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/cmd/fleetctl/fleetctl/testing_utils"
|
||||
activity_api "github.com/fleetdm/fleet/v4/server/activity/api"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/service"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -25,7 +26,8 @@ func TestHostTransferFlagChecks(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestHostsTransferByHosts(t *testing.T) {
|
||||
_, ds := testing_utils.RunServerWithMockedDS(t)
|
||||
opts := &service.TestServerOpts{}
|
||||
_, ds := testing_utils.RunServerWithMockedDS(t, opts)
|
||||
|
||||
ds.HostByIdentifierFunc = func(ctx context.Context, identifier string) (*fleet.Host, error) {
|
||||
require.Equal(t, "host1", identifier)
|
||||
|
|
@ -57,9 +59,7 @@ func TestHostsTransferByHosts(t *testing.T) {
|
|||
return &fleet.TeamLite{ID: tid, Name: "team1"}, nil
|
||||
}
|
||||
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
opts.ActivityMock.NewActivityFunc = func(_ context.Context, _ *activity_api.User, activity activity_api.ActivityDetails) error {
|
||||
require.IsType(t, fleet.ActivityTypeTransferredHostsToTeam{}, activity)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -73,7 +73,7 @@ func TestHostsTransferByHosts(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "", RunAppForTest(t, []string{"hosts", "transfer", "--team", "team1", "--hosts", "host1"}))
|
||||
assert.True(t, ds.AddHostsToTeamFuncInvoked)
|
||||
assert.True(t, ds.NewActivityFuncInvoked)
|
||||
assert.True(t, opts.ActivityMock.NewActivityFuncInvoked)
|
||||
|
||||
// Now, transfer out of the team.
|
||||
ds.AddHostsToTeamFunc = func(ctx context.Context, params *fleet.AddHostsToTeamParams) error {
|
||||
|
|
@ -81,15 +81,16 @@ func TestHostsTransferByHosts(t *testing.T) {
|
|||
assert.Equal(t, []uint{42}, params.HostIDs)
|
||||
return nil
|
||||
}
|
||||
ds.NewActivityFuncInvoked = false
|
||||
opts.ActivityMock.NewActivityFuncInvoked = false
|
||||
ds.AddHostsToTeamFuncInvoked = false
|
||||
assert.Equal(t, "", RunAppForTest(t, []string{"hosts", "transfer", "--team", "", "--hosts", "host1"}))
|
||||
assert.True(t, ds.AddHostsToTeamFuncInvoked)
|
||||
assert.True(t, ds.NewActivityFuncInvoked)
|
||||
assert.True(t, opts.ActivityMock.NewActivityFuncInvoked)
|
||||
}
|
||||
|
||||
func TestHostsTransferByLabel(t *testing.T) {
|
||||
_, ds := testing_utils.RunServerWithMockedDS(t)
|
||||
opts := &service.TestServerOpts{}
|
||||
_, ds := testing_utils.RunServerWithMockedDS(t, opts)
|
||||
|
||||
ds.HostByIdentifierFunc = func(ctx context.Context, identifier string) (*fleet.Host, error) {
|
||||
require.Equal(t, "host1", identifier)
|
||||
|
|
@ -132,9 +133,7 @@ func TestHostsTransferByLabel(t *testing.T) {
|
|||
return &fleet.TeamLite{ID: tid, Name: "team1"}, nil
|
||||
}
|
||||
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
opts.ActivityMock.NewActivityFunc = func(_ context.Context, _ *activity_api.User, activity activity_api.ActivityDetails) error {
|
||||
require.IsType(t, fleet.ActivityTypeTransferredHostsToTeam{}, activity)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -144,7 +143,7 @@ func TestHostsTransferByLabel(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.Equal(t, "", RunAppForTest(t, []string{"hosts", "transfer", "--team", "team1", "--label", "label1"}))
|
||||
require.True(t, ds.NewActivityFuncInvoked)
|
||||
require.True(t, opts.ActivityMock.NewActivityFuncInvoked)
|
||||
assert.True(t, ds.AddHostsToTeamFuncInvoked)
|
||||
|
||||
// Now, transfer out of the team.
|
||||
|
|
@ -153,15 +152,16 @@ func TestHostsTransferByLabel(t *testing.T) {
|
|||
require.Equal(t, []uint{32, 12}, params.HostIDs)
|
||||
return nil
|
||||
}
|
||||
ds.NewActivityFuncInvoked = false
|
||||
opts.ActivityMock.NewActivityFuncInvoked = false
|
||||
ds.AddHostsToTeamFuncInvoked = false
|
||||
assert.Equal(t, "", RunAppForTest(t, []string{"hosts", "transfer", "--team", "", "--label", "label1"}))
|
||||
assert.True(t, ds.AddHostsToTeamFuncInvoked)
|
||||
assert.True(t, ds.NewActivityFuncInvoked)
|
||||
assert.True(t, opts.ActivityMock.NewActivityFuncInvoked)
|
||||
}
|
||||
|
||||
func TestHostsTransferByStatus(t *testing.T) {
|
||||
_, ds := testing_utils.RunServerWithMockedDS(t)
|
||||
opts := &service.TestServerOpts{}
|
||||
_, ds := testing_utils.RunServerWithMockedDS(t, opts)
|
||||
|
||||
ds.HostByIdentifierFunc = func(ctx context.Context, identifier string) (*fleet.Host, error) {
|
||||
require.Equal(t, "host1", identifier)
|
||||
|
|
@ -203,9 +203,7 @@ func TestHostsTransferByStatus(t *testing.T) {
|
|||
return &fleet.TeamLite{ID: tid, Name: "team1"}, nil
|
||||
}
|
||||
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
opts.ActivityMock.NewActivityFunc = func(_ context.Context, _ *activity_api.User, activity activity_api.ActivityDetails) error {
|
||||
require.IsType(t, fleet.ActivityTypeTransferredHostsToTeam{}, activity)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -216,11 +214,12 @@ func TestHostsTransferByStatus(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "", RunAppForTest(t,
|
||||
[]string{"hosts", "transfer", "--team", "team1", "--status", "online"}))
|
||||
require.True(t, ds.NewActivityFuncInvoked)
|
||||
require.True(t, opts.ActivityMock.NewActivityFuncInvoked)
|
||||
}
|
||||
|
||||
func TestHostsTransferByStatusAndSearchQuery(t *testing.T) {
|
||||
_, ds := testing_utils.RunServerWithMockedDS(t)
|
||||
opts := &service.TestServerOpts{}
|
||||
_, ds := testing_utils.RunServerWithMockedDS(t, opts)
|
||||
|
||||
ds.HostByIdentifierFunc = func(ctx context.Context, identifier string) (*fleet.Host, error) {
|
||||
require.Equal(t, "host1", identifier)
|
||||
|
|
@ -263,9 +262,7 @@ func TestHostsTransferByStatusAndSearchQuery(t *testing.T) {
|
|||
return &fleet.TeamLite{ID: tid, Name: "team1"}, nil
|
||||
}
|
||||
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
opts.ActivityMock.NewActivityFunc = func(_ context.Context, _ *activity_api.User, activity activity_api.ActivityDetails) error {
|
||||
require.IsType(t, fleet.ActivityTypeTransferredHostsToTeam{}, activity)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -276,5 +273,5 @@ func TestHostsTransferByStatusAndSearchQuery(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "", RunAppForTest(t,
|
||||
[]string{"hosts", "transfer", "--team", "team1", "--status", "online", "--search_query", "somequery"}))
|
||||
require.True(t, ds.NewActivityFuncInvoked)
|
||||
require.True(t, opts.ActivityMock.NewActivityFuncInvoked)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1412,11 +1412,6 @@ func setupDSMocks(ds *mock.Store, hostByUUID map[string]testhost, hostsByID map[
|
|||
|
||||
return h.mdmInfo, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.IsHostDiskEncryptionKeyArchivedFunc = func(ctx context.Context, hostID uint) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,9 +100,6 @@ func RunServerWithMockedDS(t *testing.T, opts ...*service.TestServerOpts) (*http
|
|||
ID: 1,
|
||||
}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
ds.GetEnrollSecretsFunc = func(ctx context.Context, teamID *uint) ([]*fleet.EnrollSecret, error) { return nil, nil }
|
||||
apnsCert, apnsKey, err := mysql.GenerateTestCertBytes(mdmtesting.NewTestMDMAppleCertTemplate())
|
||||
require.NoError(t, err)
|
||||
|
|
@ -380,11 +377,6 @@ func SetupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig,
|
|||
ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) {
|
||||
return nil, 0, 0, nil, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.NewMDMAppleConfigProfileFunc = func(ctx context.Context, p fleet.MDMAppleConfigProfile, vars []fleet.FleetVarName) (*fleet.MDMAppleConfigProfile, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,17 +10,19 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/cmd/fleetctl/fleetctl/testing_utils"
|
||||
activity_api "github.com/fleetdm/fleet/v4/server/activity/api"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/service"
|
||||
"github.com/fleetdm/fleet/v4/server/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUserDelete(t *testing.T) {
|
||||
_, ds := testing_utils.RunServerWithMockedDS(t)
|
||||
opts := &service.TestServerOpts{}
|
||||
_, ds := testing_utils.RunServerWithMockedDS(t, opts)
|
||||
|
||||
ds.UserByEmailFunc = func(ctx context.Context, email string) (*fleet.User, error) {
|
||||
return &fleet.User{
|
||||
|
|
@ -41,9 +43,7 @@ func TestUserDelete(t *testing.T) {
|
|||
deletedUser = id
|
||||
return nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
opts.ActivityMock.NewActivityFunc = func(_ context.Context, _ *activity_api.User, activity activity_api.ActivityDetails) error {
|
||||
assert.Equal(t, fleet.ActivityTypeDeletedUser{}.ActivityName(), activity.ActivityName())
|
||||
return nil
|
||||
}
|
||||
|
|
@ -63,11 +63,6 @@ func TestUserCreateForcePasswordReset(t *testing.T) {
|
|||
ds.InviteByEmailFunc = func(ctx context.Context, email string) (*fleet.Invite, error) {
|
||||
return nil, ¬FoundError{}
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.UserByEmailFunc = func(ctx context.Context, email string) (*fleet.User, error) {
|
||||
if email == "bar@example.com" {
|
||||
apiOnlyUser := &fleet.User{
|
||||
|
|
@ -160,11 +155,6 @@ func TestCreateBulkUsers(t *testing.T) {
|
|||
ds.InviteByEmailFunc = func(ctx context.Context, email string) (*fleet.Invite, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.TeamsSummaryFunc = func(ctx context.Context) ([]*fleet.TeamSummary, error) {
|
||||
team1 := &fleet.TeamSummary{
|
||||
ID: 1,
|
||||
|
|
@ -193,11 +183,6 @@ func TestCreateBulkUsers(t *testing.T) {
|
|||
|
||||
func TestDeleteBulkUsers(t *testing.T) {
|
||||
_, ds := testing_utils.RunServerWithMockedDS(t)
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
csvFilePath := writeTmpCsv(t,
|
||||
`Email
|
||||
user11@example.com
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import (
|
|||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/WatchBeam/clock"
|
||||
eeservice "github.com/fleetdm/fleet/v4/ee/server/service"
|
||||
|
|
@ -104,6 +103,10 @@ func setupMockDatastorePremiumService(t testing.TB) (*mock.Store, *eeservice.Ser
|
|||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Using a noop activity service since this test does not currently verify activity creation.
|
||||
freeSvc.SetActivityService(&mock.MockNewActivityService{
|
||||
NewActivityFunc: mock.NoopNewActivityFunc,
|
||||
})
|
||||
svc, err := eeservice.NewService(
|
||||
freeSvc,
|
||||
ds,
|
||||
|
|
@ -187,9 +190,6 @@ func TestGetOrCreatePreassignTeam(t *testing.T) {
|
|||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return appConfig, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, u *fleet.User, a fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
|
||||
for _, team := range teamStore {
|
||||
if team.Name == name {
|
||||
|
|
|
|||
|
|
@ -16,18 +16,20 @@ import (
|
|||
// FleetServiceAdapter provides access to Fleet service methods
|
||||
// for data that the activity bounded context doesn't own.
|
||||
type FleetServiceAdapter struct {
|
||||
svc fleet.LookupService
|
||||
svc fleet.ActivityLookupService
|
||||
}
|
||||
|
||||
// NewFleetServiceAdapter creates a new adapter for the Fleet service.
|
||||
func NewFleetServiceAdapter(svc fleet.LookupService) *FleetServiceAdapter {
|
||||
func NewFleetServiceAdapter(svc fleet.ActivityLookupService) *FleetServiceAdapter {
|
||||
return &FleetServiceAdapter{svc: svc}
|
||||
}
|
||||
|
||||
// Ensure FleetServiceAdapter implements activity.UserProvider and activity.HostProvider
|
||||
// Ensure FleetServiceAdapter implements the required interfaces
|
||||
var (
|
||||
_ activity.UserProvider = (*FleetServiceAdapter)(nil)
|
||||
_ activity.HostProvider = (*FleetServiceAdapter)(nil)
|
||||
_ activity.UserProvider = (*FleetServiceAdapter)(nil)
|
||||
_ activity.HostProvider = (*FleetServiceAdapter)(nil)
|
||||
_ activity.AppConfigProvider = (*FleetServiceAdapter)(nil)
|
||||
_ activity.UpcomingActivityActivator = (*FleetServiceAdapter)(nil)
|
||||
)
|
||||
|
||||
// UsersByIDs fetches users by their IDs from the Fleet service.
|
||||
|
|
@ -94,3 +96,20 @@ func convertUser(u *fleet.UserSummary) *activity.User {
|
|||
APIOnly: u.APIOnly,
|
||||
}
|
||||
}
|
||||
|
||||
// GetActivitiesWebhookConfig returns the webhook configuration for activities.
|
||||
func (a *FleetServiceAdapter) GetActivitiesWebhookConfig(ctx context.Context) (*activity.ActivitiesWebhookSettings, error) {
|
||||
settings, err := a.svc.GetActivitiesWebhookSettings(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &activity.ActivitiesWebhookSettings{
|
||||
Enable: settings.Enable,
|
||||
DestinationURL: settings.DestinationURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ActivateNextUpcomingActivity activates the next upcoming activity in the queue.
|
||||
func (a *FleetServiceAdapter) ActivateNextUpcomingActivity(ctx context.Context, hostID uint, fromCompletedExecID string) error {
|
||||
return a.svc.ActivateNextUpcomingActivityForHost(ctx, hostID, fromCompletedExecID)
|
||||
}
|
||||
|
|
|
|||
25
server/activity/api/new_activity.go
Normal file
25
server/activity/api/new_activity.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// User represents user information for activity recording.
|
||||
type User struct {
|
||||
ID uint
|
||||
Name string
|
||||
Email string
|
||||
Deleted bool
|
||||
}
|
||||
|
||||
// ActivityDetails defines the interface for activity detail types.
|
||||
type ActivityDetails interface {
|
||||
ActivityName() string
|
||||
}
|
||||
|
||||
// NewActivityService is for creating activities.
|
||||
type NewActivityService interface {
|
||||
// NewActivity creates a new activity record and fires the webhook if configured.
|
||||
// user can be nil for automation-initiated activities.
|
||||
NewActivity(ctx context.Context, user *User, activity ActivityDetails) error
|
||||
}
|
||||
|
|
@ -8,4 +8,5 @@ type Service interface {
|
|||
ListActivitiesService
|
||||
ListHostPastActivitiesService
|
||||
StreamActivitiesService
|
||||
NewActivityService
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,10 +20,11 @@ func New(
|
|||
dbConns *platform_mysql.DBConnections,
|
||||
authorizer platform_authz.Authorizer,
|
||||
providers activity.DataProviders,
|
||||
webhookSendFn activity.WebhookSendFunc,
|
||||
logger *slog.Logger,
|
||||
) (api.Service, func(authMiddleware endpoint.Middleware) eu.HandlerRoutesFunc) {
|
||||
ds := mysql.NewDatastore(dbConns, logger)
|
||||
svc := service.NewService(authorizer, ds, providers, logger)
|
||||
svc := service.NewService(authorizer, ds, providers, webhookSendFn, logger)
|
||||
|
||||
routesFn := func(authMiddleware endpoint.Middleware) eu.HandlerRoutesFunc {
|
||||
return service.GetRoutes(svc, authMiddleware)
|
||||
|
|
|
|||
48
server/activity/bootstrap/testing.go
Normal file
48
server/activity/bootstrap/testing.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/activity"
|
||||
"github.com/fleetdm/fleet/v4/server/activity/api"
|
||||
"github.com/fleetdm/fleet/v4/server/activity/internal/service"
|
||||
"github.com/fleetdm/fleet/v4/server/activity/internal/types"
|
||||
platform_authz "github.com/fleetdm/fleet/v4/server/platform/authz"
|
||||
)
|
||||
|
||||
// NewForUnitTests creates an activity NewActivityService backed by a noop store (no database required).
|
||||
func NewForUnitTests(
|
||||
providers activity.DataProviders,
|
||||
webhookSendFn activity.WebhookSendFunc,
|
||||
logger *slog.Logger,
|
||||
) api.NewActivityService {
|
||||
return service.NewService(&noopAuthorizer{}, &noopStore{}, providers, webhookSendFn, logger)
|
||||
}
|
||||
|
||||
// noopAuthorizer allows all actions (appropriate for unit tests).
|
||||
type noopAuthorizer struct{}
|
||||
|
||||
func (a *noopAuthorizer) Authorize(_ context.Context, _ platform_authz.AuthzTyper, _ platform_authz.Action) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// noopStore is a datastore that does nothing (appropriate for unit tests that only need webhook behavior).
|
||||
type noopStore struct{}
|
||||
|
||||
func (s *noopStore) ListActivities(_ context.Context, _ types.ListOptions) ([]*api.Activity, *api.PaginationMetadata, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func (s *noopStore) ListHostPastActivities(_ context.Context, _ uint, _ types.ListOptions) ([]*api.Activity, *api.PaginationMetadata, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func (s *noopStore) MarkActivitiesAsStreamed(_ context.Context, _ []uint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *noopStore) NewActivity(_ context.Context, _ *api.User, _ api.ActivityDetails, _ []byte, _ time.Time) error {
|
||||
return nil
|
||||
}
|
||||
14
server/activity/config.go
Normal file
14
server/activity/config.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package activity
|
||||
|
||||
import "context"
|
||||
|
||||
// ActivitiesWebhookSettings contains webhook settings for activities.
|
||||
type ActivitiesWebhookSettings struct {
|
||||
Enable bool
|
||||
DestinationURL string
|
||||
}
|
||||
|
||||
// AppConfigProvider provides access to app configuration needed by the activity bounded context.
|
||||
type AppConfigProvider interface {
|
||||
GetActivitiesWebhookConfig(ctx context.Context) (*ActivitiesWebhookSettings, error)
|
||||
}
|
||||
|
|
@ -36,7 +36,6 @@ func TestListActivities(t *testing.T) {
|
|||
{"MatchQuery", testListActivitiesMatchQuery},
|
||||
{"Ordering", testListActivitiesOrdering},
|
||||
{"CursorPagination", testListActivitiesCursorPagination},
|
||||
{"HostOnlyExcluded", testListActivitiesHostOnlyExcluded},
|
||||
{"HostPastActivities", testListHostPastActivities},
|
||||
{"MarkActivitiesAsStreamed", testMarkActivitiesAsStreamed},
|
||||
}
|
||||
|
|
@ -309,25 +308,6 @@ func testListActivitiesCursorPagination(t *testing.T, env *testEnv) {
|
|||
}
|
||||
}
|
||||
|
||||
func testListActivitiesHostOnlyExcluded(t *testing.T, env *testEnv) {
|
||||
ctx := t.Context()
|
||||
userID := env.InsertUser(t, "testuser", "test@example.com")
|
||||
|
||||
env.InsertActivity(t, ptr.Uint(userID), "regular_activity", map[string]any{})
|
||||
|
||||
// Create host-only activity directly (should be excluded)
|
||||
_, err := env.DB.ExecContext(ctx, `
|
||||
INSERT INTO activities (user_id, user_name, user_email, activity_type, details, created_at, host_only, streamed)
|
||||
VALUES (?, 'testuser', 'test@example.com', 'host_only_activity', '{}', NOW(), true, false)
|
||||
`, userID)
|
||||
require.NoError(t, err)
|
||||
|
||||
activities, _, err := env.ds.ListActivities(ctx, listOpts())
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, activities, 1)
|
||||
assert.Equal(t, "regular_activity", activities[0].Type)
|
||||
}
|
||||
|
||||
// Test helpers for building ListOptions
|
||||
|
||||
type listOptsFunc func(*types.ListOptions)
|
||||
|
|
|
|||
105
server/activity/internal/mysql/new_activity.go
Normal file
105
server/activity/internal/mysql/new_activity.go
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/activity/api"
|
||||
"github.com/fleetdm/fleet/v4/server/activity/internal/types"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
platform_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
// NewActivity stores an activity record in the database.
|
||||
// The webhook context key must be set in the context before calling this method.
|
||||
func (ds *Datastore) NewActivity(
|
||||
ctx context.Context, user *api.User, activity api.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
ctx, span := tracer.Start(ctx, "activity.mysql.NewActivity")
|
||||
defer span.End()
|
||||
|
||||
// Sanity check to ensure we processed activity webhook before storing the activity
|
||||
processed, _ := ctx.Value(types.ActivityWebhookContextKey).(bool)
|
||||
if !processed {
|
||||
return ctxerr.New(
|
||||
ctx, "activity webhook not processed. Please use svc.NewActivity instead of ds.NewActivity. This is a Fleet server bug.",
|
||||
)
|
||||
}
|
||||
|
||||
var userID *uint
|
||||
var userName *string
|
||||
var userEmail *string
|
||||
var fleetInitiated bool
|
||||
var hostOnly bool
|
||||
|
||||
if user != nil {
|
||||
// To support creating activities with users that were deleted. This can happen
|
||||
// for automatically installed software which uses the author of the upload as the author of
|
||||
// the installation.
|
||||
if user.ID != 0 && !user.Deleted {
|
||||
userID = &user.ID
|
||||
}
|
||||
userName = &user.Name
|
||||
userEmail = &user.Email
|
||||
}
|
||||
|
||||
if automatableActivity, ok := activity.(types.AutomatableActivity); ok && automatableActivity.WasFromAutomation() {
|
||||
automationAuthor := types.ActivityAutomationAuthor
|
||||
userName = &automationAuthor
|
||||
fleetInitiated = true
|
||||
}
|
||||
|
||||
if hostOnlyActivity, ok := activity.(types.ActivityHostOnly); ok && hostOnlyActivity.HostOnly() {
|
||||
hostOnly = true
|
||||
}
|
||||
|
||||
cols := []string{"fleet_initiated", "user_id", "user_name", "activity_type", "details", "created_at", "host_only"}
|
||||
args := []any{
|
||||
fleetInitiated,
|
||||
userID,
|
||||
userName,
|
||||
activity.ActivityName(),
|
||||
details,
|
||||
createdAt,
|
||||
hostOnly,
|
||||
}
|
||||
// For system/automated activities (user == nil), user_email defaults to empty (not null).
|
||||
if userEmail != nil {
|
||||
args = append(args, userEmail)
|
||||
cols = append(cols, "user_email")
|
||||
}
|
||||
|
||||
return platform_mysql.WithRetryTxx(ctx, ds.primary, func(tx sqlx.ExtContext) error {
|
||||
const insertActStmt = `INSERT INTO activities (%s) VALUES (%s)`
|
||||
sqlStmt := fmt.Sprintf(insertActStmt, strings.Join(cols, ","), strings.Repeat("?,", len(cols)-1)+"?")
|
||||
res, err := tx.ExecContext(ctx, sqlStmt, args...)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "new activity")
|
||||
}
|
||||
|
||||
// Insert into host_activities table if the activity is associated with hosts.
|
||||
// This supposes a reasonable amount of hosts per activity, to revisit if we
|
||||
// get in the 10K+.
|
||||
if ah, ok := activity.(types.ActivityHosts); ok {
|
||||
const insertActHostStmt = `INSERT INTO host_activities (host_id, activity_id) VALUES `
|
||||
|
||||
var sb strings.Builder
|
||||
if hostIDs := ah.HostIDs(); len(hostIDs) > 0 {
|
||||
sb.WriteString(insertActHostStmt)
|
||||
actID, _ := res.LastInsertId()
|
||||
for _, hid := range hostIDs {
|
||||
sb.WriteString(fmt.Sprintf("(%d, %d),", hid, actID))
|
||||
}
|
||||
|
||||
stmt := strings.TrimSuffix(sb.String(), ",")
|
||||
if _, err := tx.ExecContext(ctx, stmt); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "insert host activity")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}, ds.logger)
|
||||
}
|
||||
246
server/activity/internal/mysql/new_activity_test.go
Normal file
246
server/activity/internal/mysql/new_activity_test.go
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/activity/api"
|
||||
"github.com/fleetdm/fleet/v4/server/activity/internal/testutils"
|
||||
"github.com/fleetdm/fleet/v4/server/activity/internal/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewActivity(t *testing.T) {
|
||||
tdb := testutils.SetupTestDB(t, "activity_new")
|
||||
ds := NewDatastore(tdb.Conns(), tdb.Logger)
|
||||
env := &testEnv{TestDB: tdb, ds: ds}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
fn func(t *testing.T, env *testEnv)
|
||||
}{
|
||||
{"WebhookContextKeyRequired", testNewActivityWebhookContextKeyRequired},
|
||||
{"BasicWithUser", testNewActivityBasicWithUser},
|
||||
{"NilUser", testNewActivityNilUser},
|
||||
{"AutomationActivity", testNewActivityAutomation},
|
||||
{"HostAssociation", testNewActivityHostAssociation},
|
||||
{"HostOnly", testNewActivityHostOnly},
|
||||
{"DeletedUser", testNewActivityDeletedUser},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
defer env.TruncateTables(t)
|
||||
c.fn(t, env)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// dummyActivity is a minimal ActivityDetails implementation for testing.
|
||||
type dummyActivity struct {
|
||||
name string
|
||||
details map[string]any
|
||||
}
|
||||
|
||||
func (d dummyActivity) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(d.details)
|
||||
}
|
||||
|
||||
func (d dummyActivity) ActivityName() string {
|
||||
return d.name
|
||||
}
|
||||
|
||||
// automatableActivity is a test activity that satisfies types.AutomatableActivity.
|
||||
type automatableActivity struct {
|
||||
dummyActivity
|
||||
}
|
||||
|
||||
func (a automatableActivity) WasFromAutomation() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// hostActivity is a test activity that satisfies types.ActivityHosts.
|
||||
type hostActivity struct {
|
||||
dummyActivity
|
||||
hostIDs []uint
|
||||
}
|
||||
|
||||
func (h hostActivity) HostIDs() []uint {
|
||||
return h.hostIDs
|
||||
}
|
||||
|
||||
// hostOnlyActivity is a test activity that satisfies types.ActivityHostOnly.
|
||||
type hostOnlyActivity struct {
|
||||
dummyActivity
|
||||
}
|
||||
|
||||
func (h hostOnlyActivity) HostOnly() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// webhookCtx returns a context with the webhook key set, as required by NewActivity.
|
||||
func webhookCtx(t *testing.T) context.Context {
|
||||
return context.WithValue(t.Context(), types.ActivityWebhookContextKey, true)
|
||||
}
|
||||
|
||||
func testNewActivityWebhookContextKeyRequired(t *testing.T, env *testEnv) {
|
||||
ctx := t.Context()
|
||||
userID := env.InsertUser(t, "test", "test@example.com")
|
||||
user := &api.User{ID: userID, Name: "test", Email: "test@example.com"}
|
||||
activity := dummyActivity{name: "test", details: map[string]any{"key": "val"}}
|
||||
detailsJSON, err := json.Marshal(activity)
|
||||
require.NoError(t, err)
|
||||
|
||||
// No webhook context key set; should fail
|
||||
assert.Error(t, env.ds.NewActivity(ctx, user, activity, detailsJSON, time.Now()))
|
||||
|
||||
// Wrong context value type; should fail
|
||||
badCtx := context.WithValue(ctx, types.ActivityWebhookContextKey, "wrong")
|
||||
assert.Error(t, env.ds.NewActivity(badCtx, user, activity, detailsJSON, time.Now()))
|
||||
|
||||
// Correct context key; should succeed
|
||||
assert.NoError(t, env.ds.NewActivity(webhookCtx(t), user, activity, detailsJSON, time.Now()))
|
||||
}
|
||||
|
||||
func testNewActivityBasicWithUser(t *testing.T, env *testEnv) {
|
||||
ctx := webhookCtx(t)
|
||||
userID := env.InsertUser(t, "fullname", "email@example.com")
|
||||
user := &api.User{ID: userID, Name: "fullname", Email: "email@example.com"}
|
||||
|
||||
details := map[string]any{"detail": 1, "sometext": "aaa"}
|
||||
detailsJSON, err := json.Marshal(details)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, env.ds.NewActivity(ctx, user, dummyActivity{name: "test_one", details: details}, detailsJSON, time.Now()))
|
||||
require.NoError(t, env.ds.NewActivity(ctx, user, dummyActivity{name: "test_two", details: map[string]any{"detail": 2}}, mustJSON(t, map[string]any{"detail": 2}), time.Now()))
|
||||
|
||||
// Verify via listing (explicit ascending order for deterministic results)
|
||||
activities, _, err := env.ds.ListActivities(t.Context(), listOpts(withPerPage(1), withOrder("id", api.OrderAscending)))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, activities, 1)
|
||||
assert.Equal(t, "fullname", *activities[0].ActorFullName)
|
||||
assert.Equal(t, "email@example.com", *activities[0].ActorEmail)
|
||||
assert.Equal(t, "test_one", activities[0].Type)
|
||||
|
||||
// Second page
|
||||
activities, _, err = env.ds.ListActivities(t.Context(), listOpts(withPerPage(1), withPage(1), withOrder("id", api.OrderAscending)))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, activities, 1)
|
||||
assert.Equal(t, "test_two", activities[0].Type)
|
||||
|
||||
// All results
|
||||
activities, _, err = env.ds.ListActivities(t.Context(), listOpts(withOrder("id", api.OrderAscending)))
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, activities, 2)
|
||||
}
|
||||
|
||||
func testNewActivityNilUser(t *testing.T, env *testEnv) {
|
||||
ctx := webhookCtx(t)
|
||||
details := map[string]any{"detail": 1}
|
||||
detailsJSON := mustJSON(t, details)
|
||||
|
||||
require.NoError(t, env.ds.NewActivity(ctx, nil, dummyActivity{name: "system_task", details: details}, detailsJSON, time.Now()))
|
||||
|
||||
activities, _, err := env.ds.ListActivities(t.Context(), listOpts())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, activities, 1)
|
||||
assert.Nil(t, activities[0].ActorID)
|
||||
assert.Nil(t, activities[0].ActorFullName)
|
||||
// user_email defaults to empty string (NOT NULL DEFAULT '') when no user is provided
|
||||
require.NotNil(t, activities[0].ActorEmail)
|
||||
assert.Empty(t, *activities[0].ActorEmail)
|
||||
assert.Equal(t, "system_task", activities[0].Type)
|
||||
}
|
||||
|
||||
func testNewActivityAutomation(t *testing.T, env *testEnv) {
|
||||
ctx := webhookCtx(t)
|
||||
activity := automatableActivity{
|
||||
dummyActivity: dummyActivity{name: "auto_task", details: map[string]any{"automated": true}},
|
||||
}
|
||||
detailsJSON := mustJSON(t, activity.details)
|
||||
|
||||
require.NoError(t, env.ds.NewActivity(ctx, nil, activity, detailsJSON, time.Now()))
|
||||
|
||||
activities, _, err := env.ds.ListActivities(t.Context(), listOpts())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, activities, 1)
|
||||
assert.Nil(t, activities[0].ActorID)
|
||||
require.NotNil(t, activities[0].ActorFullName)
|
||||
assert.Equal(t, types.ActivityAutomationAuthor, *activities[0].ActorFullName)
|
||||
assert.True(t, activities[0].FleetInitiated)
|
||||
}
|
||||
|
||||
func testNewActivityHostAssociation(t *testing.T, env *testEnv) {
|
||||
ctx := webhookCtx(t)
|
||||
userID := env.InsertUser(t, "testuser", "test@example.com")
|
||||
user := &api.User{ID: userID, Name: "testuser", Email: "test@example.com"}
|
||||
hostID := env.InsertHost(t, "h1.local", nil)
|
||||
|
||||
activity := hostActivity{
|
||||
dummyActivity: dummyActivity{name: "ran_script", details: map[string]any{"host_id": float64(hostID)}},
|
||||
hostIDs: []uint{hostID},
|
||||
}
|
||||
detailsJSON := mustJSON(t, activity.details)
|
||||
|
||||
require.NoError(t, env.ds.NewActivity(ctx, user, activity, detailsJSON, time.Now()))
|
||||
|
||||
// Verify the activity is linked to the host via host_activities
|
||||
acts, _, err := env.ds.ListHostPastActivities(t.Context(), hostID, listOpts())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, acts, 1)
|
||||
assert.Equal(t, "ran_script", acts[0].Type)
|
||||
require.NotNil(t, acts[0].ActorFullName)
|
||||
assert.Equal(t, "testuser", *acts[0].ActorFullName)
|
||||
require.NotNil(t, acts[0].ActorEmail)
|
||||
assert.Equal(t, "test@example.com", *acts[0].ActorEmail)
|
||||
}
|
||||
|
||||
func testNewActivityHostOnly(t *testing.T, env *testEnv) {
|
||||
ctx := webhookCtx(t)
|
||||
userID := env.InsertUser(t, "testuser", "test@example.com")
|
||||
user := &api.User{ID: userID, Name: "testuser", Email: "test@example.com"}
|
||||
|
||||
// Create a regular activity and a host-only activity
|
||||
regularDetails := mustJSON(t, map[string]any{"regular": true})
|
||||
require.NoError(t, env.ds.NewActivity(ctx, user, dummyActivity{name: "regular", details: map[string]any{"regular": true}}, regularDetails, time.Now()))
|
||||
|
||||
hostOnlyDetails := mustJSON(t, map[string]any{"host_only": true})
|
||||
require.NoError(t, env.ds.NewActivity(ctx, user, hostOnlyActivity{
|
||||
dummyActivity: dummyActivity{name: "host_scoped", details: map[string]any{"host_only": true}},
|
||||
}, hostOnlyDetails, time.Now()))
|
||||
|
||||
// ListActivities excludes host-only activities
|
||||
activities, _, err := env.ds.ListActivities(t.Context(), listOpts())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, activities, 1)
|
||||
assert.Equal(t, "regular", activities[0].Type)
|
||||
}
|
||||
|
||||
func testNewActivityDeletedUser(t *testing.T, env *testEnv) {
|
||||
ctx := webhookCtx(t)
|
||||
// User with Deleted=true should have their name/email preserved but user_id set to NULL
|
||||
user := &api.User{ID: 42, Name: "deleted_user", Email: "deleted@example.com", Deleted: true}
|
||||
details := mustJSON(t, map[string]any{"detail": 1})
|
||||
|
||||
require.NoError(t, env.ds.NewActivity(ctx, user, dummyActivity{name: "post_delete", details: map[string]any{"detail": 1}}, details, time.Now()))
|
||||
|
||||
activities, _, err := env.ds.ListActivities(t.Context(), listOpts())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, activities, 1)
|
||||
// user_id should be NULL (deleted user), but name is preserved
|
||||
assert.Nil(t, activities[0].ActorID)
|
||||
require.NotNil(t, activities[0].ActorFullName)
|
||||
assert.Equal(t, "deleted_user", *activities[0].ActorFullName)
|
||||
require.NotNil(t, activities[0].ActorEmail)
|
||||
assert.Equal(t, "deleted@example.com", *activities[0].ActorEmail)
|
||||
}
|
||||
|
||||
// mustJSON marshals v and fails the test on error.
|
||||
func mustJSON(t *testing.T, v any) []byte {
|
||||
t.Helper()
|
||||
b, err := json.Marshal(v)
|
||||
require.NoError(t, err)
|
||||
return b
|
||||
}
|
||||
|
|
@ -137,3 +137,7 @@ func (m *mockService) ListHostPastActivities(_ context.Context, _ uint, _ api.Li
|
|||
func (m *mockService) StreamActivities(_ context.Context, _ api.JSONLogger) error {
|
||||
panic("mockService.StreamActivities should not be called in validation tests")
|
||||
}
|
||||
|
||||
func (m *mockService) NewActivity(_ context.Context, _ *api.User, _ api.ActivityDetails) error {
|
||||
panic("mockService.NewActivity should not be called in validation tests")
|
||||
}
|
||||
|
|
|
|||
150
server/activity/internal/service/new_activity.go
Normal file
150
server/activity/internal/service/new_activity.go
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
"github.com/fleetdm/fleet/v4/server/activity/api"
|
||||
"github.com/fleetdm/fleet/v4/server/activity/internal/types"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
eu "github.com/fleetdm/fleet/v4/server/platform/endpointer"
|
||||
platformhttp "github.com/fleetdm/fleet/v4/server/platform/http"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// webhookPayload is the payload sent to the activities webhook.
|
||||
type webhookPayload struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
ActorFullName *string `json:"actor_full_name"`
|
||||
ActorID *uint `json:"actor_id"`
|
||||
ActorEmail *string `json:"actor_email"`
|
||||
Type string `json:"type"`
|
||||
Details *json.RawMessage `json:"details"`
|
||||
}
|
||||
|
||||
// NewActivity creates a new activity record and fires the webhook if configured.
|
||||
func (s *Service) NewActivity(ctx context.Context, user *api.User, activity api.ActivityDetails) error {
|
||||
detailsBytes, err := json.Marshal(activity)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "marshaling activity details")
|
||||
}
|
||||
// Duplicate JSON keys so that stored activity details include both the
|
||||
// old and new field names (e.g. team_id and fleet_id).
|
||||
if rules := eu.ExtractAliasRules(activity); len(rules) > 0 {
|
||||
detailsBytes = eu.DuplicateJSONKeys(detailsBytes, rules, eu.DuplicateJSONKeysOpts{Compact: true})
|
||||
}
|
||||
timestamp := time.Now()
|
||||
|
||||
// Fire webhook if enabled
|
||||
webhookConfig, err := s.providers.GetActivitiesWebhookConfig(ctx)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get activities webhook config")
|
||||
}
|
||||
|
||||
if webhookConfig != nil && webhookConfig.Enable {
|
||||
s.fireActivityWebhook(ctx, user, activity, detailsBytes, timestamp, webhookConfig.DestinationURL)
|
||||
}
|
||||
|
||||
// Activate the next upcoming activity if requested by the activity type.
|
||||
// This is done before storing to avoid holding a DB transaction open during
|
||||
// potentially slow operations.
|
||||
if aa, ok := activity.(types.ActivityActivator); ok && aa.MustActivateNextUpcomingActivity() {
|
||||
hostID, cmdUUID := aa.ActivateNextUpcomingActivityArgs()
|
||||
if err := s.providers.ActivateNextUpcomingActivity(ctx, hostID, cmdUUID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "activate next upcoming activity")
|
||||
}
|
||||
}
|
||||
|
||||
// Mark context as webhook processed
|
||||
ctx = context.WithValue(ctx, types.ActivityWebhookContextKey, true)
|
||||
|
||||
return s.store.NewActivity(ctx, user, activity, detailsBytes, timestamp)
|
||||
}
|
||||
|
||||
// fireActivityWebhook sends the activity to the configured webhook URL asynchronously.
|
||||
// It uses exponential backoff with a max elapsed time of 30 minutes for retries.
|
||||
func (s *Service) fireActivityWebhook(
|
||||
ctx context.Context, user *api.User, activity api.ActivityDetails,
|
||||
detailsBytes []byte, timestamp time.Time, webhookURL string,
|
||||
) {
|
||||
var userID *uint
|
||||
var userName *string
|
||||
var userEmail *string
|
||||
activityType := activity.ActivityName()
|
||||
|
||||
if user != nil {
|
||||
// To support creating activities with users that were deleted. This can happen
|
||||
// for automatically installed software which uses the author of the upload as the author of
|
||||
// the installation.
|
||||
if user.ID != 0 && !user.Deleted {
|
||||
userID = &user.ID
|
||||
}
|
||||
userName = &user.Name
|
||||
userEmail = &user.Email
|
||||
} else if automatableActivity, ok := activity.(types.AutomatableActivity); ok && automatableActivity.WasFromAutomation() {
|
||||
automationAuthor := types.ActivityAutomationAuthor
|
||||
userName = &automationAuthor
|
||||
}
|
||||
|
||||
// Capture the parent span for linking before launching the goroutine.
|
||||
parentSpanCtx := trace.SpanContextFromContext(ctx)
|
||||
|
||||
go func() {
|
||||
// Create a root span for this async webhook delivery, linked back to the
|
||||
// originating request so traces can be correlated.
|
||||
var linkOpts []trace.SpanStartOption
|
||||
if parentSpanCtx.IsValid() {
|
||||
linkOpts = append(linkOpts, trace.WithLinks(trace.Link{SpanContext: parentSpanCtx}))
|
||||
}
|
||||
spanCtx, span := tracer.Start(
|
||||
context.Background(), "activity.webhook",
|
||||
append(linkOpts,
|
||||
trace.WithNewRoot(),
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(attribute.String("activity.type", activityType)),
|
||||
)...,
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
retryStrategy := backoff.NewExponentialBackOff()
|
||||
retryStrategy.MaxElapsedTime = 30 * time.Minute
|
||||
err := backoff.Retry(
|
||||
func() error {
|
||||
if err := s.webhookSendFn(
|
||||
spanCtx, webhookURL, &webhookPayload{
|
||||
Timestamp: timestamp,
|
||||
ActorFullName: userName,
|
||||
ActorID: userID,
|
||||
ActorEmail: userEmail,
|
||||
Type: activityType,
|
||||
Details: (*json.RawMessage)(&detailsBytes),
|
||||
},
|
||||
); err != nil {
|
||||
var statusCoder kithttp.StatusCoder
|
||||
if errors.As(err, &statusCoder) && statusCoder.StatusCode() == http.StatusTooManyRequests {
|
||||
s.logger.DebugContext(spanCtx, "fire activity webhook", slog.String("err", err.Error()))
|
||||
return err
|
||||
}
|
||||
return backoff.Permanent(err)
|
||||
}
|
||||
return nil
|
||||
}, retryStrategy,
|
||||
)
|
||||
if err != nil {
|
||||
maskedErr := platformhttp.MaskURLError(err)
|
||||
span.RecordError(maskedErr)
|
||||
s.logger.ErrorContext(spanCtx,
|
||||
fmt.Sprintf("fire activity webhook to %s", platformhttp.MaskSecretURLParams(webhookURL)),
|
||||
slog.String("err", maskedErr.Error()),
|
||||
)
|
||||
}
|
||||
}()
|
||||
}
|
||||
215
server/activity/internal/service/new_activity_test.go
Normal file
215
server/activity/internal/service/new_activity_test.go
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/activity"
|
||||
"github.com/fleetdm/fleet/v4/server/activity/api"
|
||||
"github.com/fleetdm/fleet/v4/server/activity/internal/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// newActivityMockDatastore captures calls to NewActivity for assertions.
|
||||
type newActivityMockDatastore struct {
|
||||
mockDatastore
|
||||
newActivityCalled bool
|
||||
lastUser *api.User
|
||||
lastActivity api.ActivityDetails
|
||||
lastDetails []byte
|
||||
lastCreatedAt time.Time
|
||||
lastCtx context.Context
|
||||
newActivityErr error
|
||||
}
|
||||
|
||||
func (m *newActivityMockDatastore) NewActivity(ctx context.Context, user *api.User, act api.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
m.newActivityCalled = true
|
||||
m.lastUser = user
|
||||
m.lastActivity = act
|
||||
m.lastDetails = details
|
||||
m.lastCreatedAt = createdAt
|
||||
m.lastCtx = ctx
|
||||
return m.newActivityErr
|
||||
}
|
||||
|
||||
// newActivityMockProviders extends mockDataProviders with call tracking for ActivateNextUpcomingActivity.
|
||||
type newActivityMockProviders struct {
|
||||
mockDataProviders
|
||||
activateCalled bool
|
||||
lastHostID uint
|
||||
lastCmdUUID string
|
||||
activateErr error
|
||||
}
|
||||
|
||||
func (m *newActivityMockProviders) ActivateNextUpcomingActivity(ctx context.Context, hostID uint, cmdUUID string) error {
|
||||
m.activateCalled = true
|
||||
m.lastHostID = hostID
|
||||
m.lastCmdUUID = cmdUUID
|
||||
return m.activateErr
|
||||
}
|
||||
|
||||
// Test activity types
|
||||
|
||||
type simpleActivity struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (a simpleActivity) ActivityName() string { return "simple_test" }
|
||||
|
||||
type aliasedActivity struct {
|
||||
TeamID uint `json:"team_id" renameto:"fleet_id"`
|
||||
}
|
||||
|
||||
func (a aliasedActivity) ActivityName() string { return "aliased_test" }
|
||||
|
||||
type activatorActivity struct {
|
||||
simpleActivity
|
||||
hostID uint
|
||||
cmdUUID string
|
||||
}
|
||||
|
||||
func (a activatorActivity) MustActivateNextUpcomingActivity() bool { return true }
|
||||
func (a activatorActivity) ActivateNextUpcomingActivityArgs() (uint, string) {
|
||||
return a.hostID, a.cmdUUID
|
||||
}
|
||||
|
||||
func newTestService(ds types.Datastore, providers activity.DataProviders) *Service {
|
||||
noopWebhookSend := func(_ context.Context, _ string, _ any) error { return nil }
|
||||
return NewService(&mockAuthorizer{}, ds, providers, noopWebhookSend, slog.New(slog.DiscardHandler))
|
||||
}
|
||||
|
||||
func TestNewActivityStoresWithWebhookContextKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
ds := &newActivityMockDatastore{}
|
||||
providers := &newActivityMockProviders{
|
||||
mockDataProviders: mockDataProviders{
|
||||
mockUserProvider: &mockUserProvider{},
|
||||
mockHostProvider: &mockHostProvider{},
|
||||
},
|
||||
}
|
||||
svc := newTestService(ds, providers)
|
||||
|
||||
user := &api.User{ID: 1, Name: "test", Email: "test@example.com"}
|
||||
err := svc.NewActivity(t.Context(), user, simpleActivity{Name: "hello"})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify store was called
|
||||
require.True(t, ds.newActivityCalled)
|
||||
|
||||
// Verify webhook context key was set
|
||||
processed, ok := ds.lastCtx.Value(types.ActivityWebhookContextKey).(bool)
|
||||
require.True(t, ok, "webhook context key should be set")
|
||||
assert.True(t, processed)
|
||||
|
||||
// Verify user was passed through
|
||||
require.NotNil(t, ds.lastUser)
|
||||
assert.Equal(t, uint(1), ds.lastUser.ID)
|
||||
assert.Equal(t, "test", ds.lastUser.Name)
|
||||
assert.Equal(t, "test@example.com", ds.lastUser.Email)
|
||||
|
||||
// Verify details were marshaled
|
||||
var details map[string]string
|
||||
require.NoError(t, json.Unmarshal(ds.lastDetails, &details))
|
||||
assert.Equal(t, "hello", details["name"])
|
||||
|
||||
// Verify timestamp is reasonable
|
||||
assert.WithinDuration(t, time.Now(), ds.lastCreatedAt, 2*time.Second)
|
||||
}
|
||||
|
||||
func TestNewActivityDuplicatesAliasedJSONKeys(t *testing.T) {
|
||||
t.Parallel()
|
||||
ds := &newActivityMockDatastore{}
|
||||
providers := &newActivityMockProviders{
|
||||
mockDataProviders: mockDataProviders{
|
||||
mockUserProvider: &mockUserProvider{},
|
||||
mockHostProvider: &mockHostProvider{},
|
||||
},
|
||||
}
|
||||
svc := newTestService(ds, providers)
|
||||
|
||||
err := svc.NewActivity(t.Context(), nil, aliasedActivity{TeamID: 42})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, ds.newActivityCalled)
|
||||
|
||||
// Details should contain both team_id and fleet_id
|
||||
var details map[string]any
|
||||
require.NoError(t, json.Unmarshal(ds.lastDetails, &details))
|
||||
assert.Equal(t, float64(42), details["team_id"])
|
||||
assert.Equal(t, float64(42), details["fleet_id"])
|
||||
}
|
||||
|
||||
func TestNewActivityCallsActivator(t *testing.T) {
|
||||
t.Parallel()
|
||||
ds := &newActivityMockDatastore{}
|
||||
providers := &newActivityMockProviders{
|
||||
mockDataProviders: mockDataProviders{
|
||||
mockUserProvider: &mockUserProvider{},
|
||||
mockHostProvider: &mockHostProvider{},
|
||||
},
|
||||
}
|
||||
svc := newTestService(ds, providers)
|
||||
|
||||
act := activatorActivity{
|
||||
simpleActivity: simpleActivity{Name: "install"},
|
||||
hostID: 99,
|
||||
cmdUUID: "cmd-abc",
|
||||
}
|
||||
err := svc.NewActivity(t.Context(), nil, act)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify activator was called with correct args
|
||||
require.True(t, providers.activateCalled)
|
||||
assert.Equal(t, uint(99), providers.lastHostID)
|
||||
assert.Equal(t, "cmd-abc", providers.lastCmdUUID)
|
||||
|
||||
// Verify store was also called
|
||||
require.True(t, ds.newActivityCalled)
|
||||
}
|
||||
|
||||
func TestNewActivityActivatorErrorPreventsStore(t *testing.T) {
|
||||
t.Parallel()
|
||||
ds := &newActivityMockDatastore{}
|
||||
providers := &newActivityMockProviders{
|
||||
mockDataProviders: mockDataProviders{
|
||||
mockUserProvider: &mockUserProvider{},
|
||||
mockHostProvider: &mockHostProvider{},
|
||||
},
|
||||
activateErr: assert.AnError,
|
||||
}
|
||||
svc := newTestService(ds, providers)
|
||||
|
||||
act := activatorActivity{
|
||||
simpleActivity: simpleActivity{Name: "install"},
|
||||
hostID: 99,
|
||||
cmdUUID: "cmd-abc",
|
||||
}
|
||||
err := svc.NewActivity(t.Context(), nil, act)
|
||||
require.Error(t, err)
|
||||
|
||||
// Activator was called but store should NOT have been called
|
||||
require.True(t, providers.activateCalled)
|
||||
assert.False(t, ds.newActivityCalled)
|
||||
}
|
||||
|
||||
func TestNewActivityNilUser(t *testing.T) {
|
||||
t.Parallel()
|
||||
ds := &newActivityMockDatastore{}
|
||||
providers := &newActivityMockProviders{
|
||||
mockDataProviders: mockDataProviders{
|
||||
mockUserProvider: &mockUserProvider{},
|
||||
mockHostProvider: &mockHostProvider{},
|
||||
},
|
||||
}
|
||||
svc := newTestService(ds, providers)
|
||||
|
||||
err := svc.NewActivity(t.Context(), nil, simpleActivity{Name: "system"})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, ds.newActivityCalled)
|
||||
assert.Nil(t, ds.lastUser)
|
||||
}
|
||||
|
|
@ -16,8 +16,11 @@ import (
|
|||
platform_authz "github.com/fleetdm/fleet/v4/server/platform/authz"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"go.opentelemetry.io/otel"
|
||||
)
|
||||
|
||||
var tracer = otel.Tracer("github.com/fleetdm/fleet/v4/server/activity/internal/service")
|
||||
|
||||
// streamBatchSize is the number of activities to fetch per batch when streaming.
|
||||
const streamBatchSize uint = 500
|
||||
|
||||
|
|
@ -43,19 +46,27 @@ func applyListOptionsDefaults(opt *api.ListOptions, defaultOrderKey string) {
|
|||
|
||||
// Service is the activity bounded context service implementation.
|
||||
type Service struct {
|
||||
authz platform_authz.Authorizer
|
||||
store types.Datastore
|
||||
providers activity.DataProviders
|
||||
logger *slog.Logger
|
||||
authz platform_authz.Authorizer
|
||||
store types.Datastore
|
||||
providers activity.DataProviders
|
||||
webhookSendFn activity.WebhookSendFunc
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// NewService creates a new activity service.
|
||||
func NewService(authz platform_authz.Authorizer, store types.Datastore, providers activity.DataProviders, logger *slog.Logger) *Service {
|
||||
func NewService(
|
||||
authz platform_authz.Authorizer,
|
||||
store types.Datastore,
|
||||
providers activity.DataProviders,
|
||||
webhookSendFn activity.WebhookSendFunc,
|
||||
logger *slog.Logger,
|
||||
) *Service {
|
||||
return &Service{
|
||||
authz: authz,
|
||||
store: store,
|
||||
providers: providers,
|
||||
logger: logger,
|
||||
authz: authz,
|
||||
store: store,
|
||||
providers: providers,
|
||||
webhookSendFn: webhookSendFn,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"log/slog"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/activity"
|
||||
"github.com/fleetdm/fleet/v4/server/activity/api"
|
||||
|
|
@ -23,7 +24,8 @@ var (
|
|||
janeUser = &activity.User{ID: 200, Name: "Jane Smith", Email: "jane@example.com", Gravatar: "gravatar2", APIOnly: true}
|
||||
)
|
||||
|
||||
// Mock implementations
|
||||
// Bare-bones mocks for testing service logic in isolation.
|
||||
// For richer mocks used in integration tests, see tests/mocks_test.go.
|
||||
|
||||
type mockAuthorizer struct {
|
||||
authErr error
|
||||
|
|
@ -58,6 +60,10 @@ func (m *mockDatastore) MarkActivitiesAsStreamed(ctx context.Context, activityID
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *mockDatastore) NewActivity(ctx context.Context, user *api.User, activity api.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type mockUserProvider struct {
|
||||
users []*activity.User
|
||||
listUsersErr error
|
||||
|
|
@ -86,10 +92,20 @@ func (m *mockHostProvider) GetHostLite(ctx context.Context, hostID uint) (*activ
|
|||
return m.host, m.err
|
||||
}
|
||||
|
||||
// mockDataProviders combines user and host providers for testing.
|
||||
// mockDataProviders combines all provider interfaces for testing.
|
||||
type mockDataProviders struct {
|
||||
*mockUserProvider
|
||||
*mockHostProvider
|
||||
webhookConfig *activity.ActivitiesWebhookSettings
|
||||
webhookErr error
|
||||
}
|
||||
|
||||
func (m *mockDataProviders) GetActivitiesWebhookConfig(ctx context.Context) (*activity.ActivitiesWebhookSettings, error) {
|
||||
return m.webhookConfig, m.webhookErr
|
||||
}
|
||||
|
||||
func (m *mockDataProviders) ActivateNextUpcomingActivity(ctx context.Context, hostID uint, fromCompletedExecID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// testSetup holds test dependencies with pre-configured mocks
|
||||
|
|
@ -114,7 +130,8 @@ func setupTest(opts ...func(*testSetup)) *testSetup {
|
|||
for _, opt := range opts {
|
||||
opt(ts)
|
||||
}
|
||||
ts.svc = NewService(ts.authz, ts.ds, ts.providers, slog.New(slog.DiscardHandler))
|
||||
noopWebhookSend := func(_ context.Context, _ string, _ any) error { return nil }
|
||||
ts.svc = NewService(ts.authz, ts.ds, ts.providers, noopWebhookSend, slog.New(slog.DiscardHandler))
|
||||
return ts
|
||||
}
|
||||
|
||||
|
|
@ -219,6 +236,36 @@ func TestListActivitiesWithUserEnrichment(t *testing.T) {
|
|||
assert.Nil(t, activities[2].ActorAPIOnly)
|
||||
}
|
||||
|
||||
func TestListActivitiesDeletedUserFallsBackToStoredName(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
|
||||
storedName := "original_name"
|
||||
storedEmail := "original@example.com"
|
||||
deletedUserID := uint(999)
|
||||
|
||||
ts := setupTest(
|
||||
withActivities([]*api.Activity{
|
||||
{ID: 1, Type: "test_activity", ActorID: ptr.Uint(deletedUserID), ActorFullName: &storedName, ActorEmail: &storedEmail},
|
||||
}),
|
||||
// UsersByIDs returns nothing for the deleted user
|
||||
withUsers(nil),
|
||||
)
|
||||
|
||||
activities, _, err := ts.svc.ListActivities(ctx, api.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, activities, 1)
|
||||
|
||||
// Enrichment found no user, so the stored values from the DB are preserved
|
||||
require.NotNil(t, activities[0].ActorFullName)
|
||||
assert.Equal(t, "original_name", *activities[0].ActorFullName)
|
||||
require.NotNil(t, activities[0].ActorEmail)
|
||||
assert.Equal(t, "original@example.com", *activities[0].ActorEmail)
|
||||
// Gravatar and API-only are not stored in the activity table, so they remain nil
|
||||
assert.Nil(t, activities[0].ActorGravatar)
|
||||
assert.Nil(t, activities[0].ActorAPIOnly)
|
||||
}
|
||||
|
||||
// TestListActivitiesWithMatchQuery verifies that user search queries are properly
|
||||
// translated into user ID filters for the datastore.
|
||||
//
|
||||
|
|
@ -467,6 +514,10 @@ func (m *mockStreamingDatastore) MarkActivitiesAsStreamed(ctx context.Context, a
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *mockStreamingDatastore) NewActivity(ctx context.Context, user *api.User, activity api.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func newTestActivity(id uint, actorName string, actorID uint, actType, details string) *api.Activity {
|
||||
jsonDetails := json.RawMessage(details)
|
||||
return &api.Activity{
|
||||
|
|
@ -481,8 +532,9 @@ func newTestActivity(id uint, actorName string, actorID uint, actType, details s
|
|||
func TestStreamActivities(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
noopWebhookSend := func(_ context.Context, _ string, _ any) error { return nil }
|
||||
newStreamingService := func(ds *mockStreamingDatastore) *Service {
|
||||
return NewService(&mockAuthorizer{}, ds, &mockDataProviders{mockUserProvider: &mockUserProvider{}, mockHostProvider: &mockHostProvider{}}, slog.New(slog.DiscardHandler))
|
||||
return NewService(&mockAuthorizer{}, ds, &mockDataProviders{mockUserProvider: &mockUserProvider{}, mockHostProvider: &mockHostProvider{}}, noopWebhookSend, slog.New(slog.DiscardHandler))
|
||||
}
|
||||
|
||||
t.Run("basic streaming", func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ func (m *mockHostProvider) GetHostLite(ctx context.Context, hostID uint) (*activ
|
|||
return nil, platform_mysql.NotFound("Host").WithID(hostID)
|
||||
}
|
||||
|
||||
// mockDataProviders combines user and host providers for testing.
|
||||
// mockDataProviders combines all provider interfaces for testing.
|
||||
type mockDataProviders struct {
|
||||
*mockUserProvider
|
||||
*mockHostProvider
|
||||
|
|
@ -87,3 +87,11 @@ func newMockDataProviders() *mockDataProviders {
|
|||
mockHostProvider: newMockHostProvider(),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockDataProviders) GetActivitiesWebhookConfig(ctx context.Context) (*activity.ActivitiesWebhookSettings, error) {
|
||||
return &activity.ActivitiesWebhookSettings{Enable: false}, nil
|
||||
}
|
||||
|
||||
func (m *mockDataProviders) ActivateNextUpcomingActivity(ctx context.Context, hostID uint, fromCompletedExecID string) error {
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
|
@ -37,7 +38,8 @@ func setupIntegrationTest(t *testing.T) *integrationTestSuite {
|
|||
providers := newMockDataProviders()
|
||||
|
||||
// Create service
|
||||
svc := service.NewService(authorizer, ds, providers, tdb.Logger)
|
||||
noopWebhookSend := func(_ context.Context, _ string, _ any) error { return nil }
|
||||
svc := service.NewService(authorizer, ds, providers, noopWebhookSend, tdb.Logger)
|
||||
|
||||
// Create router with routes
|
||||
router := mux.NewRouter()
|
||||
|
|
|
|||
|
|
@ -3,10 +3,45 @@ package types
|
|||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/activity/api"
|
||||
)
|
||||
|
||||
// activityWebhookContextKeyType is the context key type used to indicate that the activity webhook
|
||||
// has been processed. This is a sanity check to ensure callers use the service layer
|
||||
// (which handles webhooks) rather than calling the datastore directly.
|
||||
type activityWebhookContextKeyType struct{}
|
||||
|
||||
// ActivityWebhookContextKey is used to mark that the webhook was processed before storing the activity.
|
||||
var ActivityWebhookContextKey = activityWebhookContextKeyType{}
|
||||
|
||||
// ActivityAutomationAuthor is the name used for the actor when an activity
|
||||
// is recorded as a result of an automated action (cron job, webhook, etc.)
|
||||
// or policy automation (i.e. triggered by a failing policy).
|
||||
const ActivityAutomationAuthor = "Fleet"
|
||||
|
||||
// AutomatableActivity indicates the activity was initiated by automation.
|
||||
type AutomatableActivity interface {
|
||||
WasFromAutomation() bool
|
||||
}
|
||||
|
||||
// ActivityHosts indicates the activity is associated with specific hosts.
|
||||
type ActivityHosts interface {
|
||||
HostIDs() []uint
|
||||
}
|
||||
|
||||
// ActivityHostOnly indicates the activity is host-scoped only.
|
||||
type ActivityHostOnly interface {
|
||||
HostOnly() bool
|
||||
}
|
||||
|
||||
// ActivityActivator indicates the activity should activate the next upcoming activity.
|
||||
type ActivityActivator interface {
|
||||
MustActivateNextUpcomingActivity() bool
|
||||
ActivateNextUpcomingActivityArgs() (hostID uint, cmdUUID string)
|
||||
}
|
||||
|
||||
// ListOptions extends api.ListOptions with internal fields used by the datastore.
|
||||
type ListOptions struct {
|
||||
api.ListOptions
|
||||
|
|
@ -45,4 +80,7 @@ type Datastore interface {
|
|||
ListActivities(ctx context.Context, opt ListOptions) ([]*api.Activity, *api.PaginationMetadata, error)
|
||||
ListHostPastActivities(ctx context.Context, hostID uint, opt ListOptions) ([]*api.Activity, *api.PaginationMetadata, error)
|
||||
MarkActivitiesAsStreamed(ctx context.Context, activityIDs []uint) error
|
||||
// NewActivity stores a new activity record in the database.
|
||||
// The webhook context key must be set in the context before calling this method.
|
||||
NewActivity(ctx context.Context, user *api.User, activity api.ActivityDetails, details []byte, createdAt time.Time) error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,23 @@
|
|||
package activity
|
||||
|
||||
// DataProviders combines providers for ACL layer.
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// UpcomingActivityActivator activates the next upcoming activity in the queue.
|
||||
// This is called when an activity implements ActivityActivator.
|
||||
type UpcomingActivityActivator interface {
|
||||
ActivateNextUpcomingActivity(ctx context.Context, hostID uint, fromCompletedExecID string) error
|
||||
}
|
||||
|
||||
// WebhookSendFunc is the function signature for sending a JSON payload to a URL.
|
||||
type WebhookSendFunc = func(ctx context.Context, url string, payload any) error
|
||||
|
||||
// DataProviders combines all external dependency interfaces for the activity
|
||||
// bounded context. The ACL adapter implements this single interface.
|
||||
type DataProviders interface {
|
||||
UserProvider
|
||||
HostProvider
|
||||
AppConfigProvider
|
||||
UpcomingActivityActivator
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,10 +140,13 @@ func maskEmail(email string) string {
|
|||
return string(parts[0][0]) + "***@" + parts[1]
|
||||
}
|
||||
|
||||
// systemUserName is the name used for the synthetic system user. It matches
|
||||
// the activity bounded context's ActivityAutomationAuthor constant ("Fleet").
|
||||
const systemUserName = "Fleet"
|
||||
|
||||
// systemUser is a synthetic user for internal system operations.
|
||||
// The Name uses ActivityAutomationAuthor to align with system-initiated activities.
|
||||
var systemUser = &fleet.User{
|
||||
Name: fleet.ActivityAutomationAuthor,
|
||||
Name: systemUserName,
|
||||
GlobalRole: ptr.String(fleet.RoleAdmin),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ func TestNewSystemContext(t *testing.T) {
|
|||
require.NotNil(t, v.User, "user should be present in viewer")
|
||||
|
||||
// Verify the system user has the expected properties
|
||||
assert.Equal(t, fleet.ActivityAutomationAuthor, v.User.Name, "system user name should match ActivityAutomationAuthor")
|
||||
assert.Equal(t, systemUserName, v.User.Name, "system user name should match ActivityAutomationAuthor")
|
||||
require.NotNil(t, v.User.GlobalRole, "system user should have a global role")
|
||||
assert.Equal(t, fleet.RoleAdmin, *v.User.GlobalRole, "system user should have admin role")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
|
|
@ -20,114 +19,6 @@ var (
|
|||
deleteIDsBatchSize = 1000
|
||||
)
|
||||
|
||||
// NewActivity stores an activity item that the user performed
|
||||
func (ds *Datastore) NewActivity(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
// Sanity check to ensure we processed activity webhook before storing the activity
|
||||
processed, _ := ctx.Value(fleet.ActivityWebhookContextKey).(bool)
|
||||
if !processed {
|
||||
return ctxerr.New(
|
||||
ctx, "activity webhook not processed. Please use svc.NewActivity instead of ds.NewActivity. This is a Fleet server bug.",
|
||||
)
|
||||
}
|
||||
|
||||
var userID *uint
|
||||
var userName *string
|
||||
var userEmail *string
|
||||
var fleetInitiated bool
|
||||
var hostOnly bool
|
||||
if user != nil {
|
||||
// To support creating activities with users that were deleted. This can happen
|
||||
// for automatically installed software which uses the author of the upload as the author of
|
||||
// the installation.
|
||||
if user.ID != 0 && !user.Deleted {
|
||||
userID = &user.ID
|
||||
}
|
||||
userName = &user.Name
|
||||
userEmail = &user.Email
|
||||
}
|
||||
if automatableActivity, ok := activity.(fleet.AutomatableActivity); ok && automatableActivity.WasFromAutomation() {
|
||||
userName = ptr.String(fleet.ActivityAutomationAuthor)
|
||||
fleetInitiated = true
|
||||
}
|
||||
|
||||
if hostOnlyActivity, ok := activity.(fleet.ActivityHostOnly); ok && hostOnlyActivity.HostOnly() {
|
||||
hostOnly = true
|
||||
}
|
||||
|
||||
cols := []string{"fleet_initiated", "user_id", "user_name", "activity_type", "details", "created_at", "host_only"}
|
||||
args := []any{
|
||||
fleetInitiated,
|
||||
userID,
|
||||
userName,
|
||||
activity.ActivityName(),
|
||||
details,
|
||||
createdAt,
|
||||
hostOnly,
|
||||
}
|
||||
if userEmail != nil {
|
||||
args = append(args, userEmail)
|
||||
cols = append(cols, "user_email")
|
||||
}
|
||||
|
||||
if aa, ok := activity.(fleet.ActivityActivator); ok && aa.MustActivateNextUpcomingActivity() {
|
||||
hostID, cmdUUID := aa.ActivateNextUpcomingActivityArgs()
|
||||
// NOTE: ideally this would be called in the same transaction as storing
|
||||
// the nanomdm command results, but the current design doesn't allow for
|
||||
// that with the nano store being a distinct entity to our datastore (we
|
||||
// should get rid of that distinction eventually, we've broken it already
|
||||
// in some places and it doesn't bring much benefit anymore).
|
||||
//
|
||||
// Instead, this gets called from CommandAndReportResults, which is
|
||||
// executed after the results have been saved in nano, but we already
|
||||
// accept this non-transactional fact for many other states we manage in
|
||||
// Fleet (wipe, lock results, setup experience results, etc. - see all
|
||||
// critical data that gets updated in CommandAndReportResults) so there's
|
||||
// no reason to treat the unified queue differently.
|
||||
//
|
||||
// This place here is a bit hacky but perfect for VPP/InHouse apps as the activity
|
||||
// gets created only when the MDM command status is in a final state
|
||||
// (success or failure), which is exactly when we want to activate the next
|
||||
// activity. Though note that on success of the MDM command, we wait until the
|
||||
// app gets verified (or it times out waiting for verification) to activate the
|
||||
// next activity, to ensure the app is actually installed.
|
||||
if _, err := ds.activateNextUpcomingActivity(ctx, ds.writer(ctx), hostID, cmdUUID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "activate next activity from VPP app install")
|
||||
}
|
||||
}
|
||||
|
||||
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||
const insertActStmt = `INSERT INTO activities (%s) VALUES (%s)`
|
||||
sql := fmt.Sprintf(insertActStmt, strings.Join(cols, ","), strings.Repeat("?,", len(cols)-1)+"?")
|
||||
res, err := tx.ExecContext(ctx, sql, args...)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "new activity")
|
||||
}
|
||||
|
||||
// this supposes a reasonable amount of hosts per activity, to revisit if we
|
||||
// get in the 10K+.
|
||||
if ah, ok := activity.(fleet.ActivityHosts); ok {
|
||||
const insertActHostStmt = `INSERT INTO host_activities (host_id, activity_id) VALUES `
|
||||
|
||||
var sb strings.Builder
|
||||
if hostIDs := ah.HostIDs(); len(hostIDs) > 0 {
|
||||
sb.WriteString(insertActHostStmt)
|
||||
actID, _ := res.LastInsertId()
|
||||
for _, hid := range hostIDs {
|
||||
sb.WriteString(fmt.Sprintf("(%d, %d),", hid, actID))
|
||||
}
|
||||
|
||||
stmt := strings.TrimSuffix(sb.String(), ",")
|
||||
if _, err := tx.ExecContext(ctx, stmt); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "insert host activity")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// ListHostUpcomingActivities returns the list of activities pending execution
|
||||
// or processing for the specific host. It is the "unified queue" of work to be
|
||||
// done on the host. That queue is "virtual" in the sense that it pulls from a
|
||||
|
|
@ -761,9 +652,9 @@ func (ds *Datastore) cancelHostUpcomingActivity(ctx context.Context, tx sqlx.Ext
|
|||
return nil, ctxerr.Wrap(ctx, err, "activate next upcoming activity")
|
||||
}
|
||||
|
||||
// creating the canceled activity must be done via svc.NewActivity (not
|
||||
// ds.NewActivity), so we return the ready-to-insert activity struct to the
|
||||
// caller and let svc do the rest.
|
||||
// creating the canceled activity must be done via svc.NewActivity, so we
|
||||
// return the ready-to-insert activity struct to the caller and let svc do
|
||||
// the rest.
|
||||
return pastAct, nil
|
||||
}
|
||||
|
||||
|
|
@ -1046,6 +937,19 @@ func (ds *Datastore) UnblockHostsUpcomingActivityQueue(ctx context.Context, maxH
|
|||
return len(blockedHostIDs), ds.activateNextUpcomingActivityForBatchOfHosts(ctx, blockedHostIDs)
|
||||
}
|
||||
|
||||
// ActivateNextUpcomingActivityForHost activates the next upcoming activity for the given host.
|
||||
// fromCompletedExecID is the execution ID of the activity that just completed (if any).
|
||||
//
|
||||
// NOTE: this intentionally does not use a transaction wrapper. The original
|
||||
// call site in NewActivity (now in the activity bounded context) also called
|
||||
// activateNextUpcomingActivity outside a transaction. See @mna's comment in
|
||||
// cab7cc15bef (2025-10-28) explaining that this non-transactional approach is
|
||||
// accepted and consistent with how other critical state updates work in Fleet.
|
||||
func (ds *Datastore) ActivateNextUpcomingActivityForHost(ctx context.Context, hostID uint, fromCompletedExecID string) error {
|
||||
_, err := ds.activateNextUpcomingActivity(ctx, ds.writer(ctx), hostID, fromCompletedExecID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (ds *Datastore) activateNextUpcomingActivityForBatchOfHosts(ctx context.Context, hostIDs []uint) error {
|
||||
const maxHostIDsPerBatch = 500
|
||||
|
||||
|
|
|
|||
|
|
@ -31,8 +31,6 @@ func TestActivity(t *testing.T) {
|
|||
fn func(t *testing.T, ds *Datastore)
|
||||
}{
|
||||
{"UsernameChange", testActivityUsernameChange},
|
||||
{"New", testActivityNew},
|
||||
{"EmptyUser", testActivityEmptyUser},
|
||||
{"ListHostUpcomingActivities", testListHostUpcomingActivities},
|
||||
{"CleanupActivitiesAndAssociatedData", testCleanupActivitiesAndAssociatedData},
|
||||
{"CleanupActivitiesAndAssociatedDataBatch", testCleanupActivitiesAndAssociatedDataBatch},
|
||||
|
|
@ -96,22 +94,22 @@ func testActivityUsernameChange(t *testing.T, ds *Datastore) {
|
|||
_, err := ds.NewUser(context.Background(), u)
|
||||
require.NoError(t, err)
|
||||
|
||||
timestamp := time.Now()
|
||||
ctx := context.WithValue(context.Background(), fleet.ActivityWebhookContextKey, true)
|
||||
apiUser := &activity_api.User{ID: u.ID, Name: u.Name, Email: u.Email}
|
||||
ctx := context.Background()
|
||||
require.NoError(
|
||||
t, ds.NewActivity(
|
||||
ctx, u, dummyActivity{
|
||||
t, activitySvc.NewActivity(
|
||||
ctx, apiUser, dummyActivity{
|
||||
name: "test1",
|
||||
details: map[string]interface{}{"detail": 1, "sometext": "aaa"},
|
||||
}, nil, timestamp,
|
||||
},
|
||||
),
|
||||
)
|
||||
require.NoError(
|
||||
t, ds.NewActivity(
|
||||
ctx, u, dummyActivity{
|
||||
t, activitySvc.NewActivity(
|
||||
ctx, apiUser, dummyActivity{
|
||||
name: "test2",
|
||||
details: map[string]interface{}{"detail": 2},
|
||||
}, nil, timestamp,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -139,108 +137,6 @@ func testActivityUsernameChange(t *testing.T, ds *Datastore) {
|
|||
assert.Nil(t, activities[0].ActorGravatar)
|
||||
}
|
||||
|
||||
func testActivityNew(t *testing.T, ds *Datastore) {
|
||||
activitySvc := NewTestActivityService(t, ds)
|
||||
|
||||
u := &fleet.User{
|
||||
Password: []byte("asd"),
|
||||
Name: "fullname",
|
||||
Email: "email@asd.com",
|
||||
GlobalRole: ptr.String(fleet.RoleObserver),
|
||||
}
|
||||
_, err := ds.NewUser(context.Background(), u)
|
||||
require.Nil(t, err)
|
||||
timestamp := time.Now()
|
||||
|
||||
activity := dummyActivity{
|
||||
name: "test0",
|
||||
details: map[string]interface{}{"detail": 1, "sometext": "aaa"},
|
||||
}
|
||||
// If we don't set the ActivityWebhookContextKey context value, the activity will not be created
|
||||
assert.Error(t, ds.NewActivity(context.Background(), u, activity, nil, timestamp))
|
||||
// If we set the context value to the wrong thing, the activity will not be created
|
||||
ctx := context.WithValue(context.Background(), fleet.ActivityWebhookContextKey, "bozo")
|
||||
assert.Error(t, ds.NewActivity(ctx, u, activity, nil, timestamp))
|
||||
|
||||
ctx = context.WithValue(context.Background(), fleet.ActivityWebhookContextKey, true)
|
||||
require.NoError(
|
||||
t, ds.NewActivity(
|
||||
ctx, u, dummyActivity{
|
||||
name: "test1",
|
||||
details: map[string]interface{}{"detail": 1, "sometext": "aaa"},
|
||||
}, nil, timestamp,
|
||||
),
|
||||
)
|
||||
require.NoError(
|
||||
t, ds.NewActivity(
|
||||
ctx, u, dummyActivity{
|
||||
name: "test2",
|
||||
details: map[string]interface{}{"detail": 2},
|
||||
}, nil, timestamp,
|
||||
),
|
||||
)
|
||||
|
||||
opt := activity_api.ListOptions{
|
||||
Page: 0,
|
||||
PerPage: 1,
|
||||
}
|
||||
activities := ListActivitiesAPI(t, context.Background(), activitySvc, opt)
|
||||
assert.Len(t, activities, 1)
|
||||
assert.Equal(t, "fullname", *activities[0].ActorFullName)
|
||||
assert.Equal(t, "test1", activities[0].Type)
|
||||
|
||||
opt = activity_api.ListOptions{
|
||||
Page: 1,
|
||||
PerPage: 1,
|
||||
}
|
||||
activities = ListActivitiesAPI(t, context.Background(), activitySvc, opt)
|
||||
assert.Len(t, activities, 1)
|
||||
assert.Equal(t, "fullname", *activities[0].ActorFullName)
|
||||
assert.Equal(t, "test2", activities[0].Type)
|
||||
|
||||
opt = activity_api.ListOptions{
|
||||
Page: 0,
|
||||
PerPage: 10,
|
||||
}
|
||||
activities = ListActivitiesAPI(t, context.Background(), activitySvc, opt)
|
||||
assert.Len(t, activities, 2)
|
||||
}
|
||||
|
||||
func testActivityEmptyUser(t *testing.T, ds *Datastore) {
|
||||
activitySvc := NewTestActivityService(t, ds)
|
||||
|
||||
timestamp := time.Now()
|
||||
ctx := context.WithValue(context.Background(), fleet.ActivityWebhookContextKey, true)
|
||||
require.NoError(
|
||||
t, ds.NewActivity(
|
||||
ctx, nil, dummyActivity{
|
||||
name: "test1",
|
||||
details: map[string]interface{}{"detail": 1, "sometext": "aaa"},
|
||||
}, nil, timestamp,
|
||||
),
|
||||
)
|
||||
|
||||
require.NoError(
|
||||
t, ds.NewActivity(
|
||||
ctx, nil, fleet.ActivityInstalledAppStoreApp{
|
||||
HostID: 1,
|
||||
HostDisplayName: "A Host",
|
||||
SoftwareTitle: "Trello",
|
||||
AppStoreID: "123456",
|
||||
CommandUUID: "some uuid",
|
||||
Status: string(fleet.SoftwareInstalled),
|
||||
SelfService: false,
|
||||
PolicyID: ptr.Uint(1),
|
||||
PolicyName: ptr.String("Sample Policy"),
|
||||
}, nil, timestamp,
|
||||
),
|
||||
)
|
||||
|
||||
activities := ListActivitiesAPI(t, context.Background(), activitySvc, activity_api.ListOptions{})
|
||||
assert.Len(t, activities, 2)
|
||||
assert.Equal(t, "Fleet", *activities[1].ActorFullName)
|
||||
}
|
||||
|
||||
func testListHostUpcomingActivities(t *testing.T, ds *Datastore) {
|
||||
noUserCtx := context.Background()
|
||||
|
||||
|
|
@ -682,33 +578,28 @@ func testCleanupActivitiesAndAssociatedData(t *testing.T, ds *Datastore) {
|
|||
Type: fleet.TargetHost,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
timestamp := time.Now()
|
||||
ctx = context.WithValue(context.Background(), fleet.ActivityWebhookContextKey, true)
|
||||
err = ds.NewActivity(ctx, user1, dummyActivity{
|
||||
apiUser := &activity_api.User{ID: user1.ID, Name: user1.Name, Email: user1.Email}
|
||||
err = activitySvc.NewActivity(ctx, apiUser, dummyActivity{
|
||||
name: "other activity",
|
||||
details: map[string]interface{}{"detail": 0, "foo": "zoo"},
|
||||
}, nil, timestamp,
|
||||
)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
err = ds.NewActivity(ctx, user1, dummyActivity{
|
||||
err = activitySvc.NewActivity(ctx, apiUser, dummyActivity{
|
||||
name: "live query",
|
||||
details: map[string]interface{}{"detail": 1, "foo": "bar"},
|
||||
}, nil, timestamp,
|
||||
)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
err = ds.NewActivity(ctx, user1, dummyActivity{
|
||||
err = activitySvc.NewActivity(ctx, apiUser, dummyActivity{
|
||||
name: "some host activity",
|
||||
details: map[string]interface{}{"detail": 0, "foo": "zoo"},
|
||||
hostIDs: []uint{1},
|
||||
}, nil, timestamp,
|
||||
)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
err = ds.NewActivity(ctx, user1, dummyActivity{
|
||||
err = activitySvc.NewActivity(ctx, apiUser, dummyActivity{
|
||||
name: "some host activity 2",
|
||||
details: map[string]interface{}{"detail": 0, "foo": "bar"},
|
||||
hostIDs: []uint{2},
|
||||
}, nil, timestamp,
|
||||
)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Nothing is deleted, as the activities and associated data is recent.
|
||||
|
|
@ -878,8 +769,8 @@ func testCleanupActivitiesAndAssociatedDataBatch(t *testing.T, ds *Datastore) {
|
|||
}
|
||||
|
||||
func testActivateNextActivity(t *testing.T, ds *Datastore) {
|
||||
activitySvc := NewTestActivityService(t, ds)
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, fleet.ActivityWebhookContextKey, true)
|
||||
|
||||
test.CreateInsertGlobalVPPToken(t, ds)
|
||||
|
||||
|
|
@ -1064,11 +955,11 @@ func testActivateNextActivity(t *testing.T, ds *Datastore) {
|
|||
err = nanoDB.StoreCommandReport(nanoCtx, cmdRes)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.NewActivity(ctx, nil, fleet.ActivityInstalledAppStoreApp{
|
||||
err = activitySvc.NewActivity(ctx, nil, fleet.ActivityInstalledAppStoreApp{
|
||||
HostID: h1.ID,
|
||||
AppStoreID: vppApp1.VPPAppTeam.AdamID,
|
||||
CommandUUID: vpp1_1,
|
||||
}, []byte(`{}`), time.Now())
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
appleCmdRes, err := ds.GetMDMAppleCommandResults(ctx, vpp1_1, "")
|
||||
|
|
@ -1145,11 +1036,11 @@ func testActivateNextActivity(t *testing.T, ds *Datastore) {
|
|||
err = nanoDB.StoreCommandReport(nanoCtx, cmdRes)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.NewActivity(ctx, nil, fleet.ActivityInstalledAppStoreApp{
|
||||
err = activitySvc.NewActivity(ctx, nil, fleet.ActivityInstalledAppStoreApp{
|
||||
HostID: h1.ID,
|
||||
AppStoreID: vppApp2.VPPAppTeam.AdamID,
|
||||
CommandUUID: vpp1_2,
|
||||
}, []byte(`{}`), time.Now())
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
appleCmdRes, err = ds.GetMDMAppleCommandResults(ctx, vpp1_2, "")
|
||||
|
|
@ -1248,12 +1139,12 @@ func testActivateNextActivity(t *testing.T, ds *Datastore) {
|
|||
err = nanoDB.StoreCommandReport(nanoCtx, cmdRes)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.NewActivity(ctx, nil, fleet.ActivityInstalledAppStoreApp{
|
||||
err = activitySvc.NewActivity(ctx, nil, fleet.ActivityInstalledAppStoreApp{
|
||||
HostID: hIOS.ID,
|
||||
AppStoreID: vppApp1IOS.VPPAppTeam.AdamID,
|
||||
CommandUUID: vpp1_1_ios,
|
||||
Status: "Error", // using a failure because otherwise it requires verification to activate next
|
||||
}, []byte(`{}`), time.Now())
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// the in-house app is now activated
|
||||
|
|
@ -1291,11 +1182,11 @@ func testActivateNextActivity(t *testing.T, ds *Datastore) {
|
|||
err = nanoDB.StoreCommandReport(nanoCtx, cmdRes)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.NewActivity(ctx, nil, &fleet.ActivityTypeInstalledSoftware{
|
||||
err = activitySvc.NewActivity(ctx, nil, &fleet.ActivityTypeInstalledSoftware{
|
||||
HostID: hIOS.ID,
|
||||
CommandUUID: ihaCmd,
|
||||
Status: "Error", // using a failure because otherwise it requires verification to activate next
|
||||
}, []byte(`{}`), time.Now())
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
pendingActs, _, err = ds.ListHostUpcomingActivities(ctx, hIOS.ID, fleet.ListOptions{})
|
||||
|
|
@ -1323,11 +1214,11 @@ func testActivateNextActivity(t *testing.T, ds *Datastore) {
|
|||
err = nanoDB.StoreCommandReport(nanoCtx, cmdRes)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.NewActivity(ctx, nil, &fleet.ActivityTypeInstalledSoftware{
|
||||
err = activitySvc.NewActivity(ctx, nil, &fleet.ActivityTypeInstalledSoftware{
|
||||
HostID: hIOS.ID,
|
||||
CommandUUID: vpp1_1_ios,
|
||||
Status: string(fleet.SoftwareInstalled),
|
||||
}, []byte(`{}`), time.Now())
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// both are still upcoming...
|
||||
|
|
@ -1355,11 +1246,11 @@ func testActivateNextActivity(t *testing.T, ds *Datastore) {
|
|||
err = nanoDB.StoreCommandReport(nanoCtx, cmdRes)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.NewActivity(ctx, nil, &fleet.ActivityTypeInstalledSoftware{
|
||||
err = activitySvc.NewActivity(ctx, nil, &fleet.ActivityTypeInstalledSoftware{
|
||||
HostID: hIOS.ID,
|
||||
CommandUUID: ihaCmd,
|
||||
Status: string(fleet.SoftwareInstalled),
|
||||
}, []byte(`{}`), time.Now())
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
pendingActs, _, err = ds.ListHostUpcomingActivities(ctx, hIOS.ID, fleet.ListOptions{})
|
||||
|
|
@ -1377,8 +1268,8 @@ func testActivateNextActivity(t *testing.T, ds *Datastore) {
|
|||
}
|
||||
|
||||
func testActivateItselfOnEmptyQueue(t *testing.T, ds *Datastore) {
|
||||
activitySvc := NewTestActivityService(t, ds)
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, fleet.ActivityWebhookContextKey, true)
|
||||
test.CreateInsertGlobalVPPToken(t, ds)
|
||||
|
||||
h1 := test.NewHost(t, ds, "h1.local", "10.10.10.1", "1", "1", time.Now())
|
||||
|
|
@ -1464,11 +1355,11 @@ func testActivateItselfOnEmptyQueue(t *testing.T, ds *Datastore) {
|
|||
}
|
||||
err = nanoDB.StoreCommandReport(nanoCtx, cmdRes)
|
||||
require.NoError(t, err)
|
||||
err = ds.NewActivity(ctx, nil, fleet.ActivityInstalledAppStoreApp{
|
||||
err = activitySvc.NewActivity(ctx, nil, fleet.ActivityInstalledAppStoreApp{
|
||||
HostID: h1.ID,
|
||||
AppStoreID: vppApp1.VPPAppTeam.AdamID,
|
||||
CommandUUID: vpp1_1,
|
||||
}, []byte(`{}`), time.Now())
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// the upcoming queue should be empty, each result having emptied the list
|
||||
|
|
@ -1479,6 +1370,14 @@ func testActivateItselfOnEmptyQueue(t *testing.T, ds *Datastore) {
|
|||
}
|
||||
|
||||
func testCancelNonActivatedUpcomingActivity(t *testing.T, ds *Datastore) {
|
||||
activitySvc := NewTestActivityService(t, ds)
|
||||
newActivityFn := func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
||||
var apiUser *activity_api.User
|
||||
if user != nil {
|
||||
apiUser = &activity_api.User{ID: user.ID, Name: user.Name, Email: user.Email}
|
||||
}
|
||||
return activitySvc.NewActivity(ctx, apiUser, activity)
|
||||
}
|
||||
ctx := context.Background()
|
||||
test.CreateInsertGlobalVPPToken(t, ds)
|
||||
|
||||
|
|
@ -1580,7 +1479,7 @@ func testCancelNonActivatedUpcomingActivity(t *testing.T, ds *Datastore) {
|
|||
exec2 := test.CreateHostScriptUpcomingActivity(t, ds, host)
|
||||
exec3 := test.CreateHostSoftwareInstallUpcomingActivity(t, ds, host, u)
|
||||
t.Cleanup(func() {
|
||||
test.SetHostVPPAppInstallResult(t, ds, nanoDB, host, exec1, adamID, "Acknowledged")
|
||||
test.SetHostVPPAppInstallResult(t, ds, nanoDB, host, exec1, adamID, "Acknowledged", newActivityFn)
|
||||
test.SetHostSoftwareInstallResult(t, ds, host, exec3, 0)
|
||||
})
|
||||
return []string{exec1, exec2, exec3}
|
||||
|
|
@ -1609,7 +1508,7 @@ func testCancelNonActivatedUpcomingActivity(t *testing.T, ds *Datastore) {
|
|||
exec1, adamID := test.CreateHostVPPAppInstallUpcomingActivity(t, ds, hostIOS)
|
||||
exec2 := test.CreateHostInHouseAppInstallUpcomingActivity(t, ds, hostIOS, u)
|
||||
t.Cleanup(func() {
|
||||
test.SetHostVPPAppInstallResult(t, ds, nanoDB, host, exec1, adamID, "Acknowledged")
|
||||
test.SetHostVPPAppInstallResult(t, ds, nanoDB, host, exec1, adamID, "Acknowledged", newActivityFn)
|
||||
})
|
||||
return []string{exec1, exec2}
|
||||
},
|
||||
|
|
@ -1645,6 +1544,14 @@ func testCancelNonActivatedUpcomingActivity(t *testing.T, ds *Datastore) {
|
|||
}
|
||||
|
||||
func testCancelActivatedUpcomingActivity(t *testing.T, ds *Datastore) {
|
||||
activitySvc := NewTestActivityService(t, ds)
|
||||
newActivityFn := func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
||||
var apiUser *activity_api.User
|
||||
if user != nil {
|
||||
apiUser = &activity_api.User{ID: user.ID, Name: user.Name, Email: user.Email}
|
||||
}
|
||||
return activitySvc.NewActivity(ctx, apiUser, activity)
|
||||
}
|
||||
ctx := context.Background()
|
||||
test.CreateInsertGlobalVPPToken(t, ds)
|
||||
|
||||
|
|
@ -1707,7 +1614,7 @@ func testCancelActivatedUpcomingActivity(t *testing.T, ds *Datastore) {
|
|||
exec1 := test.CreateHostSoftwareUninstallUpcomingActivity(t, ds, host, u)
|
||||
exec2, adamID := test.CreateHostVPPAppInstallUpcomingActivity(t, ds, host)
|
||||
t.Cleanup(func() {
|
||||
test.SetHostVPPAppInstallResult(t, ds, nanoDB, host, exec2, adamID, "Acknowledged")
|
||||
test.SetHostVPPAppInstallResult(t, ds, nanoDB, host, exec2, adamID, "Acknowledged", newActivityFn)
|
||||
})
|
||||
return []string{exec1, exec2}
|
||||
},
|
||||
|
|
@ -1761,7 +1668,7 @@ func testCancelActivatedUpcomingActivity(t *testing.T, ds *Datastore) {
|
|||
exec1, _ := test.CreateHostVPPAppInstallUpcomingActivity(t, ds, host)
|
||||
exec2, adamID := test.CreateHostVPPAppInstallUpcomingActivity(t, ds, host)
|
||||
t.Cleanup(func() {
|
||||
test.SetHostVPPAppInstallResult(t, ds, nanoDB, host, exec2, adamID, "Acknowledged")
|
||||
test.SetHostVPPAppInstallResult(t, ds, nanoDB, host, exec2, adamID, "Acknowledged", newActivityFn)
|
||||
})
|
||||
return []string{exec1, exec2}
|
||||
},
|
||||
|
|
@ -1773,7 +1680,7 @@ func testCancelActivatedUpcomingActivity(t *testing.T, ds *Datastore) {
|
|||
exec1 := test.CreateHostInHouseAppInstallUpcomingActivity(t, ds, hostIOS, u)
|
||||
exec2, adamID := test.CreateHostVPPAppInstallUpcomingActivity(t, ds, hostIOS)
|
||||
t.Cleanup(func() {
|
||||
test.SetHostVPPAppInstallResult(t, ds, nanoDB, hostIOS, exec2, adamID, "Acknowledged")
|
||||
test.SetHostVPPAppInstallResult(t, ds, nanoDB, hostIOS, exec2, adamID, "Acknowledged", newActivityFn)
|
||||
})
|
||||
return []string{exec1, exec2}
|
||||
},
|
||||
|
|
@ -1785,7 +1692,7 @@ func testCancelActivatedUpcomingActivity(t *testing.T, ds *Datastore) {
|
|||
exec1 := test.CreateHostInHouseAppInstallUpcomingActivity(t, ds, hostIOS, u)
|
||||
exec2 := test.CreateHostInHouseAppInstallUpcomingActivity(t, ds, hostIOS, u)
|
||||
t.Cleanup(func() {
|
||||
test.SetHostInHouseAppInstallResult(t, ds, nanoDB, hostIOS, exec2, "Acknowledged")
|
||||
test.SetHostInHouseAppInstallResult(t, ds, nanoDB, hostIOS, exec2, "Acknowledged", newActivityFn)
|
||||
})
|
||||
return []string{exec1, exec2}
|
||||
},
|
||||
|
|
@ -1850,6 +1757,14 @@ func testCancelActivatedUpcomingActivity(t *testing.T, ds *Datastore) {
|
|||
}
|
||||
|
||||
func testSetResultAfterCancelUpcomingActivity(t *testing.T, ds *Datastore) {
|
||||
activitySvc := NewTestActivityService(t, ds)
|
||||
newActivityFn := func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
||||
var apiUser *activity_api.User
|
||||
if user != nil {
|
||||
apiUser = &activity_api.User{ID: user.ID, Name: user.Name, Email: user.Email}
|
||||
}
|
||||
return activitySvc.NewActivity(ctx, apiUser, activity)
|
||||
}
|
||||
ctx := context.Background()
|
||||
test.CreateInsertGlobalVPPToken(t, ds)
|
||||
|
||||
|
|
@ -1881,7 +1796,7 @@ func testSetResultAfterCancelUpcomingActivity(t *testing.T, ds *Datastore) {
|
|||
exec, adamID := test.CreateHostVPPAppInstallUpcomingActivity(t, ds, host)
|
||||
_, err = ds.CancelHostUpcomingActivity(ctx, host.ID, exec)
|
||||
require.NoError(t, err)
|
||||
test.SetHostVPPAppInstallResult(t, ds, nanoDB, host, exec, adamID, "Acknowledged")
|
||||
test.SetHostVPPAppInstallResult(t, ds, nanoDB, host, exec, adamID, "Acknowledged", newActivityFn)
|
||||
}
|
||||
|
||||
func testGetHostUpcomingActivityMeta(t *testing.T, ds *Datastore) {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import (
|
|||
"github.com/WatchBeam/clock"
|
||||
"github.com/fleetdm/fleet/v4/pkg/optjson"
|
||||
"github.com/fleetdm/fleet/v4/server"
|
||||
activity_api "github.com/fleetdm/fleet/v4/server/activity/api"
|
||||
"github.com/fleetdm/fleet/v4/server/config"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/license"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
|
|
@ -8698,16 +8699,13 @@ func testHostsDeleteHosts(t *testing.T, ds *Datastore) {
|
|||
HostID: host.ID,
|
||||
HostDisplayName: host.DisplayName(),
|
||||
}
|
||||
detailsBytes, err := json.Marshal(activity)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx = context.WithValue(ctx, fleet.ActivityWebhookContextKey, true)
|
||||
err = ds.NewActivity( // automatically creates the host_activities entry
|
||||
activitySvc := NewTestActivityService(t, ds)
|
||||
apiUser := &activity_api.User{ID: user1.ID, Name: user1.Name, Email: user1.Email}
|
||||
err = activitySvc.NewActivity( // automatically creates the host_activities entry
|
||||
ctx,
|
||||
user1,
|
||||
apiUser,
|
||||
activity,
|
||||
detailsBytes,
|
||||
time.Now(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
|
|||
|
|
@ -464,7 +464,8 @@ func createInHouseAppInstallRequest(t *testing.T, ds *Datastore, hostID uint, ap
|
|||
|
||||
func createInHouseAppInstallResult(t *testing.T, ds *Datastore, host *fleet.Host, cmdUUID string, status string) {
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, fleet.ActivityWebhookContextKey, true)
|
||||
|
||||
activitySvc := NewTestActivityService(t, ds)
|
||||
|
||||
nanoDB, err := nanomdm_mysql.New(nanomdm_mysql.WithDB(ds.primary.DB))
|
||||
require.NoError(t, err)
|
||||
|
|
@ -480,10 +481,10 @@ func createInHouseAppInstallResult(t *testing.T, ds *Datastore, host *fleet.Host
|
|||
|
||||
// inserting the activity is what marks the upcoming activity as completed
|
||||
// (and activates the next one).
|
||||
err = ds.NewActivity(ctx, nil, fleet.ActivityInstalledAppStoreApp{
|
||||
err = activitySvc.NewActivity(ctx, nil, fleet.ActivityInstalledAppStoreApp{
|
||||
HostID: host.ID,
|
||||
CommandUUID: cmdUUID,
|
||||
}, []byte(`{}`), time.Now())
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
|
|
@ -1803,7 +1804,11 @@ func testInHouseAppsCancelledOnUnenroll(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
for i, act := range activitiesToCreate {
|
||||
// caller's responsibility to create new activities
|
||||
require.NoError(t, ds.NewActivity(ctx, users[i], act, nil, time.Now()))
|
||||
var apiUser *activity_api.User
|
||||
if users[i] != nil {
|
||||
apiUser = &activity_api.User{ID: users[i].ID, Name: users[i].Name, Email: users[i].Email}
|
||||
}
|
||||
require.NoError(t, activitySvc.NewActivity(ctx, apiUser, act))
|
||||
}
|
||||
|
||||
// fleet needs to receive some command result at some
|
||||
|
|
|
|||
|
|
@ -32,16 +32,12 @@ import (
|
|||
nano_push "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push"
|
||||
scep_depot "github.com/fleetdm/fleet/v4/server/mdm/scep/depot"
|
||||
common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql"
|
||||
"github.com/fleetdm/fleet/v4/server/service/modules/activities"
|
||||
"github.com/go-sql-driver/mysql"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/jmoiron/sqlx"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.39.0"
|
||||
)
|
||||
|
||||
// Compile-time interface check
|
||||
var _ activities.ActivityStore = (*Datastore)(nil)
|
||||
|
||||
const (
|
||||
mySQLTimestampFormat = "2006-01-02 15:04:05" // %Y/%m/%d %H:%M:%S
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/WatchBeam/clock"
|
||||
"github.com/fleetdm/fleet/v4/server"
|
||||
"github.com/fleetdm/fleet/v4/server/acl/activityacl"
|
||||
activity_api "github.com/fleetdm/fleet/v4/server/activity/api"
|
||||
activity_bootstrap "github.com/fleetdm/fleet/v4/server/activity/bootstrap"
|
||||
|
|
@ -429,14 +430,7 @@ func CreateNamedMySQLDSWithConns(t *testing.T, name string) (*Datastore, *common
|
|||
ds := initializeDatabase(t, name, new(testing_utils.DatastoreTestOptions))
|
||||
t.Cleanup(func() { ds.Close() })
|
||||
|
||||
replica, ok := ds.replica.(*sqlx.DB)
|
||||
require.True(t, ok, "ds.replica should be *sqlx.DB in tests")
|
||||
dbConns := &common_mysql.DBConnections{
|
||||
Primary: ds.primary,
|
||||
Replica: replica,
|
||||
}
|
||||
|
||||
return ds, dbConns
|
||||
return ds, TestDBConnections(t, ds)
|
||||
}
|
||||
|
||||
func ExecAdhocSQL(tb testing.TB, ds *Datastore, fn func(q sqlx.ExtContext) error) {
|
||||
|
|
@ -986,7 +980,7 @@ func (t *testingAuthorizer) Authorize(_ context.Context, _ platform_authz.AuthzT
|
|||
return nil
|
||||
}
|
||||
|
||||
// testingLookupService adapts mysql.Datastore to fleet.LookupService interface.
|
||||
// testingLookupService adapts mysql.Datastore to fleet.ActivityLookupService interface.
|
||||
// This allows tests to use the real activityacl.FleetServiceAdapter instead of
|
||||
// duplicating the conversion logic.
|
||||
type testingLookupService struct {
|
||||
|
|
@ -1006,22 +1000,39 @@ func (t *testingLookupService) GetHostLite(ctx context.Context, id uint) (*fleet
|
|||
return t.ds.HostLite(ctx, id)
|
||||
}
|
||||
|
||||
func (t *testingLookupService) GetActivitiesWebhookSettings(ctx context.Context) (fleet.ActivitiesWebhookSettings, error) {
|
||||
appConfig, err := t.ds.AppConfig(ctx)
|
||||
if err != nil {
|
||||
return fleet.ActivitiesWebhookSettings{}, err
|
||||
}
|
||||
return appConfig.WebhookSettings.ActivitiesWebhook, nil
|
||||
}
|
||||
|
||||
func (t *testingLookupService) ActivateNextUpcomingActivityForHost(ctx context.Context, hostID uint, fromCompletedExecID string) error {
|
||||
return t.ds.ActivateNextUpcomingActivityForHost(ctx, hostID, fromCompletedExecID)
|
||||
}
|
||||
|
||||
// TestDBConnections extracts the underlying DB connections from a test Datastore.
|
||||
func TestDBConnections(t testing.TB, ds *Datastore) *common_mysql.DBConnections {
|
||||
t.Helper()
|
||||
replica, ok := ds.replica.(*sqlx.DB)
|
||||
require.True(t, ok, "ds.replica should be *sqlx.DB in tests")
|
||||
return &common_mysql.DBConnections{Primary: ds.primary, Replica: replica}
|
||||
}
|
||||
|
||||
// NewTestActivityService creates an activity service. This allows tests to call the activity bounded context API.
|
||||
// User data is fetched from the same database to support tests that verify user info in activities.
|
||||
func NewTestActivityService(t testing.TB, ds *Datastore) activity_api.Service {
|
||||
t.Helper()
|
||||
|
||||
// Extract DB connections
|
||||
replica, ok := ds.replica.(*sqlx.DB)
|
||||
require.True(t, ok, "ds.replica should be *sqlx.DB in tests")
|
||||
dbConns := &common_mysql.DBConnections{Primary: ds.primary, Replica: replica}
|
||||
dbConns := TestDBConnections(t, ds)
|
||||
|
||||
// Use the real ACL adapter with a testing lookup service
|
||||
lookupSvc := &testingLookupService{ds: ds}
|
||||
providers := activityacl.NewFleetServiceAdapter(lookupSvc)
|
||||
aclAdapter := activityacl.NewFleetServiceAdapter(lookupSvc)
|
||||
|
||||
// Create service via bootstrap (the public API for creating the bounded context)
|
||||
svc, _ := activity_bootstrap.New(dbConns, &testingAuthorizer{}, providers, slog.New(slog.DiscardHandler))
|
||||
svc, _ := activity_bootstrap.New(dbConns, &testingAuthorizer{}, aclAdapter, server.PostJSONWithTimeout, slog.New(slog.DiscardHandler))
|
||||
return svc
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -498,7 +498,8 @@ func createVPPAppInstallRequest(t *testing.T, ds *Datastore, host *fleet.Host, a
|
|||
|
||||
func createVPPAppInstallResult(t *testing.T, ds *Datastore, host *fleet.Host, cmdUUID string, status string) {
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, fleet.ActivityWebhookContextKey, true)
|
||||
|
||||
activitySvc := NewTestActivityService(t, ds)
|
||||
|
||||
nanoDB, err := nanomdm_mysql.New(nanomdm_mysql.WithDB(ds.primary.DB))
|
||||
require.NoError(t, err)
|
||||
|
|
@ -514,10 +515,10 @@ func createVPPAppInstallResult(t *testing.T, ds *Datastore, host *fleet.Host, cm
|
|||
|
||||
// inserting the activity is what marks the upcoming activity as completed
|
||||
// (and activates the next one).
|
||||
err = ds.NewActivity(ctx, nil, fleet.ActivityInstalledAppStoreApp{
|
||||
err = activitySvc.NewActivity(ctx, nil, fleet.ActivityInstalledAppStoreApp{
|
||||
HostID: host.ID,
|
||||
CommandUUID: cmdUUID,
|
||||
}, []byte(`{}`), time.Now())
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ import (
|
|||
|
||||
type ContextKey string
|
||||
|
||||
// NewActivityFunc is the function signature for creating a new activity.
|
||||
type NewActivityFunc func(ctx context.Context, user *User, activity ActivityDetails) error
|
||||
|
||||
type ActivityWebhookPayload struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
ActorFullName *string `json:"actor_full_name"`
|
||||
|
|
@ -20,11 +23,6 @@ type ActivityWebhookPayload struct {
|
|||
// ActivityWebhookContextKey is the context key to indicate that the activity webhook has been processed before saving the activity.
|
||||
const ActivityWebhookContextKey = ContextKey("ActivityWebhook")
|
||||
|
||||
// ActivityAutomationAuthor is the name used for the actor when an activity
|
||||
// is performed by Fleet automation (cron jobs, system operations, etc.)
|
||||
// rather than by a human user.
|
||||
const ActivityAutomationAuthor = "Fleet"
|
||||
|
||||
type Activity struct {
|
||||
CreateTimestamp
|
||||
|
||||
|
|
|
|||
|
|
@ -785,12 +785,14 @@ type Datastore interface {
|
|||
///////////////////////////////////////////////////////////////////////////////
|
||||
// ActivitiesStore
|
||||
|
||||
NewActivity(ctx context.Context, user *User, activity ActivityDetails, details []byte, createdAt time.Time) error
|
||||
ListHostUpcomingActivities(ctx context.Context, hostID uint, opt ListOptions) ([]*UpcomingActivity, *PaginationMetadata, error)
|
||||
CancelHostUpcomingActivity(ctx context.Context, hostID uint, executionID string) (ActivityDetails, error)
|
||||
IsExecutionPendingForHost(ctx context.Context, hostID uint, scriptID uint) (bool, error)
|
||||
GetHostUpcomingActivityMeta(ctx context.Context, hostID uint, executionID string) (*UpcomingActivityMeta, error)
|
||||
UnblockHostsUpcomingActivityQueue(ctx context.Context, maxHosts int) (int, error)
|
||||
// ActivateNextUpcomingActivityForHost activates the next upcoming activity for the given host.
|
||||
// fromCompletedExecID is the execution ID of the activity that just completed (if any).
|
||||
ActivateNextUpcomingActivityForHost(ctx context.Context, hostID uint, fromCompletedExecID string) error
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// StatisticsStore
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/activity/api"
|
||||
"github.com/fleetdm/fleet/v4/server/version"
|
||||
"github.com/fleetdm/fleet/v4/server/websocket"
|
||||
)
|
||||
|
|
@ -96,10 +97,20 @@ type LookupService interface {
|
|||
HostLookupService
|
||||
}
|
||||
|
||||
// ActivityLookupService extends LookupService with methods needed by the
|
||||
// activity bounded context's ACL adapter.
|
||||
type ActivityLookupService interface {
|
||||
LookupService
|
||||
|
||||
// GetActivitiesWebhookSettings returns the webhook settings for activities.
|
||||
GetActivitiesWebhookSettings(ctx context.Context) (ActivitiesWebhookSettings, error)
|
||||
// ActivateNextUpcomingActivityForHost activates the next upcoming activity for the given host.
|
||||
ActivateNextUpcomingActivityForHost(ctx context.Context, hostID uint, fromCompletedExecID string) error
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
OsqueryService
|
||||
UserLookupService
|
||||
HostLookupService
|
||||
ActivityLookupService
|
||||
|
||||
// GetTransparencyURL gets the URL to redirect to when an end user clicks About Fleet
|
||||
GetTransparencyURL(ctx context.Context) (string, error)
|
||||
|
|
@ -631,6 +642,10 @@ type Service interface {
|
|||
// /////////////////////////////////////////////////////////////////////////////
|
||||
// ActivitiesService
|
||||
|
||||
// SetActivityService sets the activity bounded context service for creating activities.
|
||||
// This should be called after service creation to inject the activity service dependency.
|
||||
SetActivityService(activitySvc api.NewActivityService)
|
||||
|
||||
// NewActivity creates the given activity on the datastore.
|
||||
//
|
||||
// What we call "Activities" are administrative operations,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/authz"
|
||||
"github.com/fleetdm/fleet/v4/server/config"
|
||||
|
|
@ -17,7 +16,6 @@ import (
|
|||
ds_mock "github.com/fleetdm/fleet/v4/server/mock"
|
||||
"github.com/fleetdm/fleet/v4/server/platform/logging"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/fleetdm/fleet/v4/server/service/modules/activities"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
@ -30,7 +28,7 @@ func TestEnterprisesAuth(t *testing.T) {
|
|||
androidAPIClient.InitCommonMocks()
|
||||
logger := logging.NewLogfmtLogger(os.Stdout)
|
||||
fleetDS := InitCommonDSMocks()
|
||||
activityModule := activities.NewActivityModule(fleetDS, logger)
|
||||
activityModule := &noopActivityModule{} // This test does not verify activity creation.
|
||||
svc, err := NewServiceWithClient(logger.SlogLogger(), fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule, config.AndroidAgentConfig{})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -128,7 +126,7 @@ func TestEnterpriseSignupMissingPrivateKey(t *testing.T) {
|
|||
androidAPIClient.InitCommonMocks()
|
||||
logger := logging.NewLogfmtLogger(os.Stdout)
|
||||
fleetDS := InitCommonDSMocks()
|
||||
activityModule := activities.NewActivityModule(fleetDS, logger)
|
||||
activityModule := &noopActivityModule{} // This test does not verify activity creation.
|
||||
|
||||
svc, err := NewServiceWithClient(logger.SlogLogger(), fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule, config.AndroidAgentConfig{})
|
||||
require.NoError(t, err)
|
||||
|
|
@ -212,9 +210,6 @@ func InitCommonDSMocks() *AndroidMockDS {
|
|||
ds.Store.BulkSetAndroidHostsUnenrolledFunc = func(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
ds.Store.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
ds.Store.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
|
||||
return &fleet.Job{}, nil
|
||||
}
|
||||
|
|
@ -236,6 +231,13 @@ type notFoundError struct{}
|
|||
func (e *notFoundError) Error() string { return "not found" }
|
||||
func (e *notFoundError) IsNotFound() bool { return true }
|
||||
|
||||
// noopActivityModule is a no-op implementation of the ActivityModule interface.
|
||||
type noopActivityModule struct{}
|
||||
|
||||
func (n *noopActivityModule) NewActivity(_ context.Context, _ *fleet.User, _ fleet.ActivityDetails) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestGetEnterprise(t *testing.T) {
|
||||
logger := logging.NewLogfmtLogger(os.Stdout)
|
||||
user := &fleet.User{ID: 1, GlobalRole: ptr.String(fleet.RoleAdmin)}
|
||||
|
|
@ -246,7 +248,7 @@ func TestGetEnterprise(t *testing.T) {
|
|||
androidAPIClient.InitCommonMocks()
|
||||
|
||||
fleetDS := InitCommonDSMocks()
|
||||
activityModule := activities.NewActivityModule(fleetDS, logger)
|
||||
activityModule := &noopActivityModule{} // This test does not verify activity creation.
|
||||
svc, err := NewServiceWithClient(logger.SlogLogger(), fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule, config.AndroidAgentConfig{})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -266,7 +268,7 @@ func TestGetEnterprise(t *testing.T) {
|
|||
return nil, ¬FoundError{}
|
||||
}
|
||||
|
||||
activityModule := activities.NewActivityModule(fleetDS, logger)
|
||||
activityModule := &noopActivityModule{} // This test does not verify activity creation.
|
||||
svc, err := NewServiceWithClient(logger.SlogLogger(), fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule, config.AndroidAgentConfig{})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -312,7 +314,7 @@ func TestVerifyExistingEnterpriseIfAny(t *testing.T) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
activityModule := activities.NewActivityModule(fleetDS, logger)
|
||||
activityModule := &noopActivityModule{} // This test does not verify activity creation.
|
||||
svc, err := NewServiceWithClient(logger.SlogLogger(), fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule, config.AndroidAgentConfig{})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -361,7 +363,7 @@ func TestVerifyExistingEnterpriseIfAny(t *testing.T) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
activityModule := activities.NewActivityModule(fleetDS, logger)
|
||||
activityModule := &noopActivityModule{} // This test does not verify activity creation.
|
||||
svc, err := NewServiceWithClient(logger.SlogLogger(), fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule, config.AndroidAgentConfig{})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -402,7 +404,7 @@ func TestVerifyExistingEnterpriseIfAny(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
activityModule := activities.NewActivityModule(fleetDS, logger)
|
||||
activityModule := &noopActivityModule{} // This test does not verify activity creation.
|
||||
svc, err := NewServiceWithClient(logger.SlogLogger(), fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule, config.AndroidAgentConfig{})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -443,7 +445,7 @@ func TestVerifyExistingEnterpriseIfAny(t *testing.T) {
|
|||
return []*androidmanagement.Enterprise{}, nil
|
||||
}
|
||||
|
||||
activityModule := activities.NewActivityModule(fleetDS, logger)
|
||||
activityModule := &noopActivityModule{} // This test does not verify activity creation.
|
||||
svc, err := NewServiceWithClient(logger.SlogLogger(), fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule, config.AndroidAgentConfig{})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -510,7 +512,7 @@ func TestVerifyExistingEnterpriseIfAny(t *testing.T) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
activityModule := activities.NewActivityModule(fleetDS, logger)
|
||||
activityModule := &noopActivityModule{} // This test does not verify activity creation.
|
||||
svc, err := NewServiceWithClient(logger.SlogLogger(), fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule, config.AndroidAgentConfig{})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -540,7 +542,7 @@ func TestVerifyExistingEnterpriseIfAny(t *testing.T) {
|
|||
return nil, ¬FoundError{}
|
||||
}
|
||||
|
||||
activityModule := activities.NewActivityModule(fleetDS, logger)
|
||||
activityModule := &noopActivityModule{} // This test does not verify activity creation.
|
||||
svc, err := NewServiceWithClient(logger.SlogLogger(), fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule, config.AndroidAgentConfig{})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
|
|||
|
|
@ -17,10 +17,8 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/android"
|
||||
android_mock "github.com/fleetdm/fleet/v4/server/mdm/android/mock"
|
||||
"github.com/fleetdm/fleet/v4/server/platform/logging"
|
||||
common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/fleetdm/fleet/v4/server/service/modules/activities"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/api/androidmanagement/v1"
|
||||
|
|
@ -34,7 +32,7 @@ func createAndroidService(t *testing.T) (android.Service, *AndroidMockDS) {
|
|||
androidAPIClient.InitCommonMocks()
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
mockDS := InitCommonDSMocks()
|
||||
activityModule := activities.NewActivityModule(mockDS, logging.NewLogger(logger))
|
||||
activityModule := &noopActivityModule{} // This test does not verify activity creation.
|
||||
svc, err := NewServiceWithClient(logger, mockDS, &androidAPIClient, "test-private-key", &mockDS.DataStore, activityModule, config.AndroidAgentConfig{})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
|
|
@ -13,7 +12,7 @@ import (
|
|||
// ReconcileAndroidDevices polls AMAPI for devices that Fleet still considers enrolled
|
||||
// and flips them to unenrolled if Google reports them missing (404).
|
||||
// This complements (does not replace) Pub/Sub DELETED handling.
|
||||
func ReconcileAndroidDevices(ctx context.Context, ds fleet.Datastore, logger *slog.Logger, licenseKey string) error {
|
||||
func ReconcileAndroidDevices(ctx context.Context, ds fleet.Datastore, logger *slog.Logger, licenseKey string, newActivityFn fleet.NewActivityFunc) error {
|
||||
appConfig, err := ds.AppConfig(ctx)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get app config")
|
||||
|
|
@ -88,12 +87,12 @@ func ReconcileAndroidDevices(ctx context.Context, ds fleet.Datastore, logger *sl
|
|||
displayName = hosts[0].DisplayName()
|
||||
serial = hosts[0].HardwareSerial
|
||||
}
|
||||
if aerr := ds.NewActivity(ctx, nil, fleet.ActivityTypeMDMUnenrolled{
|
||||
if aerr := newActivityFn(ctx, nil, fleet.ActivityTypeMDMUnenrolled{
|
||||
HostSerial: serial,
|
||||
HostDisplayName: displayName,
|
||||
InstalledFromDEP: false,
|
||||
Platform: "android",
|
||||
}, nil, time.Now()); aerr != nil {
|
||||
}); aerr != nil {
|
||||
logger.DebugContext(ctx, "failed to create mdm_unenrolled activity during android reconcile", "host_id", dev.HostID, "err", aerr)
|
||||
}
|
||||
unenrolled++
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import (
|
|||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/config"
|
||||
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
|
||||
|
|
@ -23,7 +22,6 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/fleetdm/fleet/v4/server/service/middleware/auth"
|
||||
"github.com/fleetdm/fleet/v4/server/service/middleware/log"
|
||||
"github.com/fleetdm/fleet/v4/server/service/modules/activities"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
|
@ -90,6 +88,13 @@ func (ds *AndroidDSWithMock) SetAndroidHostUnenrolled(ctx context.Context, hostI
|
|||
return ds.Datastore.SetAndroidHostUnenrolled(ctx, hostID)
|
||||
}
|
||||
|
||||
// noopActivityModule implements activities.ActivityModule with a no-op for tests.
|
||||
type noopActivityModule struct{}
|
||||
|
||||
func (n *noopActivityModule) NewActivity(_ context.Context, _ *fleet.User, _ fleet.ActivityDetails) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type WithServer struct {
|
||||
suite.Suite
|
||||
Svc android.Service
|
||||
|
|
@ -114,9 +119,8 @@ func (ts *WithServer) SetupSuite(t *testing.T, dbName string) {
|
|||
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
kitLogger := logging.NewLogger(logger)
|
||||
activityModule := activities.NewActivityModule(&ts.DS.DataStore, kitLogger)
|
||||
svc, err := service.NewServiceWithClient(logger, &ts.DS, &ts.AndroidAPIClient, "test-private-key", ts.DS.Datastore, activityModule,
|
||||
config.AndroidAgentConfig{})
|
||||
activityModule := &noopActivityModule{} // This test does not verify activity creation.
|
||||
svc, err := service.NewServiceWithClient(logger, &ts.DS, &ts.AndroidAPIClient, "test-private-key", ts.DS.Datastore, activityModule, config.AndroidAgentConfig{})
|
||||
require.NoError(t, err)
|
||||
ts.Svc = svc
|
||||
|
||||
|
|
@ -158,9 +162,6 @@ func (ts *WithServer) CreateCommonDSMocks() {
|
|||
ts.DS.BulkSetAndroidHostsUnenrolledFunc = func(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
ts.DS.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *WithServer) createCommonProxyMocks(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -1393,8 +1393,8 @@ func (pb *ProfileBimap) add(wantedProfile, currentProfile *fleet.MDMAppleProfile
|
|||
pb.currentState[currentProfile] = wantedProfile
|
||||
}
|
||||
|
||||
// NewActivityFunc is the function signature for creating a new activity.
|
||||
type NewActivityFunc func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error
|
||||
// NewActivityFunc is an alias for fleet.NewActivityFunc.
|
||||
type NewActivityFunc = fleet.NewActivityFunc
|
||||
|
||||
func IOSiPadOSRefetch(ctx context.Context, ds fleet.Datastore, commander *MDMAppleCommander, logger *slog.Logger,
|
||||
newActivityFn NewActivityFunc) error {
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ type HostLifecycle struct {
|
|||
// NewActivityFunc is the signature type of the service-layer function that can
|
||||
// create activities and handle the webhook notification and all other
|
||||
// mechanisms required when creating an activity.
|
||||
type NewActivityFunc func(ctx context.Context, user *fleet.User, details fleet.ActivityDetails, ds fleet.Datastore, logger *logging.Logger) error
|
||||
type NewActivityFunc func(ctx context.Context, user *fleet.User, details fleet.ActivityDetails) error
|
||||
|
||||
// New creates a new HostLifecycle struct
|
||||
func New(ds fleet.Datastore, logger *logging.Logger, newActivityFn NewActivityFunc) *HostLifecycle {
|
||||
|
|
@ -227,7 +227,7 @@ func (t *HostLifecycle) turnOnApple(ctx context.Context, opts HostOptions) error
|
|||
} else {
|
||||
mdmEnrolledActivity.HostSerial = ptr.String(info.HardwareSerial)
|
||||
}
|
||||
err = t.newActivityFunc(ctx, nil, mdmEnrolledActivity, t.ds, t.logger)
|
||||
err = t.newActivityFunc(ctx, nil, mdmEnrolledActivity)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create mdm enrolled activity")
|
||||
}
|
||||
|
|
@ -385,7 +385,7 @@ func (t *HostLifecycle) createActivities(ctx context.Context, users []*fleet.Use
|
|||
|
||||
for i, act := range acts {
|
||||
user := users[i]
|
||||
if err := t.newActivityFunc(ctx, user, act, t.ds, t.logger); err != nil {
|
||||
if err := t.newActivityFunc(ctx, user, act); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create activity")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func nopNewActivity(ctx context.Context, user *fleet.User, details fleet.ActivityDetails, ds fleet.Datastore, logger *logging.Logger) error {
|
||||
func nopNewActivity(ctx context.Context, user *fleet.User, details fleet.ActivityDetails) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
43
server/mock/activity_mock.go
Normal file
43
server/mock/activity_mock.go
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package mock
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
activity_api "github.com/fleetdm/fleet/v4/server/activity/api"
|
||||
)
|
||||
|
||||
// NewActivityFunc is the callback function type for MockNewActivityService.
|
||||
type NewActivityFunc func(ctx context.Context, user *activity_api.User, activity activity_api.ActivityDetails) error
|
||||
|
||||
// NoopNewActivityFunc is a no-op implementation of NewActivityFunc for tests
|
||||
// that don't need to intercept activity creation.
|
||||
var NoopNewActivityFunc NewActivityFunc = func(_ context.Context, _ *activity_api.User, _ activity_api.ActivityDetails) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MockNewActivityService is a mock implementation of activity_api.NewActivityService
|
||||
// for unit tests that use mock.Store instead of real MySQL connections.
|
||||
// When Delegate is set, it is called before the mock's NewActivityFunc,
|
||||
// allowing real behavior (e.g. webhooks) while still capturing calls.
|
||||
type MockNewActivityService struct {
|
||||
NewActivityFunc NewActivityFunc // defaults to NoopNewActivityFunc if nil
|
||||
NewActivityFuncInvoked bool
|
||||
Delegate activity_api.NewActivityService
|
||||
}
|
||||
|
||||
// Ensure MockNewActivityService implements activity_api.NewActivityService.
|
||||
var _ activity_api.NewActivityService = (*MockNewActivityService)(nil)
|
||||
|
||||
func (m *MockNewActivityService) NewActivity(ctx context.Context, user *activity_api.User, activity activity_api.ActivityDetails) error {
|
||||
m.NewActivityFuncInvoked = true
|
||||
if m.Delegate != nil {
|
||||
if err := m.Delegate.NewActivity(ctx, user, activity); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fn := m.NewActivityFunc
|
||||
if fn == nil {
|
||||
fn = NoopNewActivityFunc
|
||||
}
|
||||
return fn(ctx, user, activity)
|
||||
}
|
||||
|
|
@ -585,8 +585,6 @@ type CleanupHostOperatingSystemsFunc func(ctx context.Context) error
|
|||
|
||||
type MDMTurnOffFunc func(ctx context.Context, uuid string) (users []*fleet.User, activities []fleet.ActivityDetails, err error)
|
||||
|
||||
type NewActivityFunc func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error
|
||||
|
||||
type ListHostUpcomingActivitiesFunc func(ctx context.Context, hostID uint, opt fleet.ListOptions) ([]*fleet.UpcomingActivity, *fleet.PaginationMetadata, error)
|
||||
|
||||
type CancelHostUpcomingActivityFunc func(ctx context.Context, hostID uint, executionID string) (fleet.ActivityDetails, error)
|
||||
|
|
@ -597,6 +595,8 @@ type GetHostUpcomingActivityMetaFunc func(ctx context.Context, hostID uint, exec
|
|||
|
||||
type UnblockHostsUpcomingActivityQueueFunc func(ctx context.Context, maxHosts int) (int, error)
|
||||
|
||||
type ActivateNextUpcomingActivityForHostFunc func(ctx context.Context, hostID uint, fromCompletedExecID string) error
|
||||
|
||||
type ShouldSendStatisticsFunc func(ctx context.Context, frequency time.Duration, config config.FleetConfig) (fleet.StatisticsPayload, bool, error)
|
||||
|
||||
type RecordStatisticsSentFunc func(ctx context.Context) error
|
||||
|
|
@ -2627,9 +2627,6 @@ type DataStore struct {
|
|||
MDMTurnOffFunc MDMTurnOffFunc
|
||||
MDMTurnOffFuncInvoked bool
|
||||
|
||||
NewActivityFunc NewActivityFunc
|
||||
NewActivityFuncInvoked bool
|
||||
|
||||
ListHostUpcomingActivitiesFunc ListHostUpcomingActivitiesFunc
|
||||
ListHostUpcomingActivitiesFuncInvoked bool
|
||||
|
||||
|
|
@ -2645,6 +2642,9 @@ type DataStore struct {
|
|||
UnblockHostsUpcomingActivityQueueFunc UnblockHostsUpcomingActivityQueueFunc
|
||||
UnblockHostsUpcomingActivityQueueFuncInvoked bool
|
||||
|
||||
ActivateNextUpcomingActivityForHostFunc ActivateNextUpcomingActivityForHostFunc
|
||||
ActivateNextUpcomingActivityForHostFuncInvoked bool
|
||||
|
||||
ShouldSendStatisticsFunc ShouldSendStatisticsFunc
|
||||
ShouldSendStatisticsFuncInvoked bool
|
||||
|
||||
|
|
@ -6394,13 +6394,6 @@ func (s *DataStore) MDMTurnOff(ctx context.Context, uuid string) (users []*fleet
|
|||
return s.MDMTurnOffFunc(ctx, uuid)
|
||||
}
|
||||
|
||||
func (s *DataStore) NewActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
s.mu.Lock()
|
||||
s.NewActivityFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.NewActivityFunc(ctx, user, activity, details, createdAt)
|
||||
}
|
||||
|
||||
func (s *DataStore) ListHostUpcomingActivities(ctx context.Context, hostID uint, opt fleet.ListOptions) ([]*fleet.UpcomingActivity, *fleet.PaginationMetadata, error) {
|
||||
s.mu.Lock()
|
||||
s.ListHostUpcomingActivitiesFuncInvoked = true
|
||||
|
|
@ -6436,6 +6429,13 @@ func (s *DataStore) UnblockHostsUpcomingActivityQueue(ctx context.Context, maxHo
|
|||
return s.UnblockHostsUpcomingActivityQueueFunc(ctx, maxHosts)
|
||||
}
|
||||
|
||||
func (s *DataStore) ActivateNextUpcomingActivityForHost(ctx context.Context, hostID uint, fromCompletedExecID string) error {
|
||||
s.mu.Lock()
|
||||
s.ActivateNextUpcomingActivityForHostFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.ActivateNextUpcomingActivityForHostFunc(ctx, hostID, fromCompletedExecID)
|
||||
}
|
||||
|
||||
func (s *DataStore) ShouldSendStatistics(ctx context.Context, frequency time.Duration, config config.FleetConfig) (fleet.StatisticsPayload, bool, error) {
|
||||
s.mu.Lock()
|
||||
s.ShouldSendStatisticsFuncInvoked = true
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/activity/api"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/version"
|
||||
"github.com/fleetdm/fleet/v4/server/websocket"
|
||||
|
|
@ -41,6 +42,10 @@ type UsersByIDsFunc func(ctx context.Context, ids []uint) ([]*fleet.UserSummary,
|
|||
|
||||
type GetHostLiteFunc func(ctx context.Context, id uint) (host *fleet.Host, err error)
|
||||
|
||||
type GetActivitiesWebhookSettingsFunc func(ctx context.Context) (fleet.ActivitiesWebhookSettings, error)
|
||||
|
||||
type ActivateNextUpcomingActivityForHostFunc func(ctx context.Context, hostID uint, fromCompletedExecID string) error
|
||||
|
||||
type GetTransparencyURLFunc func(ctx context.Context) (string, error)
|
||||
|
||||
type AuthenticateOrbitHostFunc func(ctx context.Context, nodeKey string) (host *fleet.Host, debug bool, err error)
|
||||
|
|
@ -385,6 +390,8 @@ type ModifyTeamEnrollSecretsFunc func(ctx context.Context, teamID uint, secrets
|
|||
|
||||
type ApplyTeamSpecsFunc func(ctx context.Context, specs []*fleet.TeamSpec, applyOpts fleet.ApplyTeamSpecOptions) (map[string]uint, error)
|
||||
|
||||
type SetActivityServiceFunc func(activitySvc api.NewActivityService)
|
||||
|
||||
type NewActivityFunc func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error
|
||||
|
||||
type ListHostUpcomingActivitiesFunc func(ctx context.Context, hostID uint, opt fleet.ListOptions) ([]*fleet.UpcomingActivity, *fleet.PaginationMetadata, error)
|
||||
|
|
@ -911,6 +918,12 @@ type Service struct {
|
|||
GetHostLiteFunc GetHostLiteFunc
|
||||
GetHostLiteFuncInvoked bool
|
||||
|
||||
GetActivitiesWebhookSettingsFunc GetActivitiesWebhookSettingsFunc
|
||||
GetActivitiesWebhookSettingsFuncInvoked bool
|
||||
|
||||
ActivateNextUpcomingActivityForHostFunc ActivateNextUpcomingActivityForHostFunc
|
||||
ActivateNextUpcomingActivityForHostFuncInvoked bool
|
||||
|
||||
GetTransparencyURLFunc GetTransparencyURLFunc
|
||||
GetTransparencyURLFuncInvoked bool
|
||||
|
||||
|
|
@ -1427,6 +1440,9 @@ type Service struct {
|
|||
ApplyTeamSpecsFunc ApplyTeamSpecsFunc
|
||||
ApplyTeamSpecsFuncInvoked bool
|
||||
|
||||
SetActivityServiceFunc SetActivityServiceFunc
|
||||
SetActivityServiceFuncInvoked bool
|
||||
|
||||
NewActivityFunc NewActivityFunc
|
||||
NewActivityFuncInvoked bool
|
||||
|
||||
|
|
@ -2245,6 +2261,20 @@ func (s *Service) GetHostLite(ctx context.Context, id uint) (host *fleet.Host, e
|
|||
return s.GetHostLiteFunc(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Service) GetActivitiesWebhookSettings(ctx context.Context) (fleet.ActivitiesWebhookSettings, error) {
|
||||
s.mu.Lock()
|
||||
s.GetActivitiesWebhookSettingsFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.GetActivitiesWebhookSettingsFunc(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) ActivateNextUpcomingActivityForHost(ctx context.Context, hostID uint, fromCompletedExecID string) error {
|
||||
s.mu.Lock()
|
||||
s.ActivateNextUpcomingActivityForHostFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.ActivateNextUpcomingActivityForHostFunc(ctx, hostID, fromCompletedExecID)
|
||||
}
|
||||
|
||||
func (s *Service) GetTransparencyURL(ctx context.Context) (string, error) {
|
||||
s.mu.Lock()
|
||||
s.GetTransparencyURLFuncInvoked = true
|
||||
|
|
@ -3449,6 +3479,13 @@ func (s *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec, a
|
|||
return s.ApplyTeamSpecsFunc(ctx, specs, applyOpts)
|
||||
}
|
||||
|
||||
func (s *Service) SetActivityService(activitySvc api.NewActivityService) {
|
||||
s.mu.Lock()
|
||||
s.SetActivityServiceFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
s.SetActivityServiceFunc(activitySvc)
|
||||
}
|
||||
|
||||
func (s *Service) NewActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
||||
s.mu.Lock()
|
||||
s.NewActivityFuncInvoked = true
|
||||
|
|
|
|||
50
server/platform/http/url.go
Normal file
50
server/platform/http/url.go
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MaskSecretURLParams masks URL query values if the query param name includes "secret", "token",
|
||||
// "key", "password". It accepts a raw string and returns a redacted string if the raw string is
|
||||
// URL-parseable. If it is not URL-parseable, the raw string is returned unchanged.
|
||||
func MaskSecretURLParams(rawURL string) string {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return rawURL
|
||||
}
|
||||
|
||||
keywords := []string{"secret", "token", "key", "password"}
|
||||
containsKeyword := func(s string) bool {
|
||||
s = strings.ToLower(s)
|
||||
for _, kw := range keywords {
|
||||
if strings.Contains(s, kw) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
q := u.Query()
|
||||
for k := range q {
|
||||
if containsKeyword(k) {
|
||||
q[k] = []string{"MASKED"}
|
||||
}
|
||||
}
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
return u.Redacted()
|
||||
}
|
||||
|
||||
// MaskURLError checks if the provided error is a *url.Error. If so, it applies MaskSecretURLParams
|
||||
// to the URL value and returns the modified error. If not, the error is returned unchanged.
|
||||
func MaskURLError(e error) error {
|
||||
var ue *url.Error
|
||||
ok := errors.As(e, &ue)
|
||||
if !ok {
|
||||
return e
|
||||
}
|
||||
ue.URL = MaskSecretURLParams(ue.URL)
|
||||
return ue
|
||||
}
|
||||
|
|
@ -2,23 +2,12 @@ package service
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
"github.com/fleetdm/fleet/v4/server"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/go-kit/log/level"
|
||||
|
||||
activity_api "github.com/fleetdm/fleet/v4/server/activity/api"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/platform/endpointer"
|
||||
"github.com/fleetdm/fleet/v4/server/platform/logging"
|
||||
)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -33,83 +22,29 @@ type listActivitiesResponse struct {
|
|||
|
||||
func (r listActivitiesResponse) Error() error { return r.Err }
|
||||
|
||||
func (svc *Service) NewActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
||||
return newActivity(ctx, user, activity, svc.ds, svc.logger)
|
||||
func (svc *Service) GetActivitiesWebhookSettings(ctx context.Context) (fleet.ActivitiesWebhookSettings, error) {
|
||||
appConfig, err := svc.ds.AppConfig(ctx)
|
||||
if err != nil {
|
||||
return fleet.ActivitiesWebhookSettings{}, ctxerr.Wrap(ctx, err, "get app config for activities webhook")
|
||||
}
|
||||
return appConfig.WebhookSettings.ActivitiesWebhook, nil
|
||||
}
|
||||
|
||||
func newActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, ds fleet.Datastore, logger *logging.Logger) error {
|
||||
appConfig, err := ds.AppConfig(ctx)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get app config")
|
||||
}
|
||||
func (svc *Service) ActivateNextUpcomingActivityForHost(ctx context.Context, hostID uint, fromCompletedExecID string) error {
|
||||
return svc.ds.ActivateNextUpcomingActivityForHost(ctx, hostID, fromCompletedExecID)
|
||||
}
|
||||
|
||||
detailsBytes, err := json.Marshal(activity)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "marshaling activity details")
|
||||
}
|
||||
// Duplicate JSON keys so that stored activity details include both the
|
||||
// old and new field names (e.g. team_id and fleet_id).
|
||||
if rules := endpointer.ExtractAliasRules(activity); len(rules) > 0 {
|
||||
detailsBytes = endpointer.DuplicateJSONKeys(detailsBytes, rules, endpointer.DuplicateJSONKeysOpts{Compact: true})
|
||||
}
|
||||
timestamp := time.Now()
|
||||
|
||||
if appConfig.WebhookSettings.ActivitiesWebhook.Enable {
|
||||
webhookURL := appConfig.WebhookSettings.ActivitiesWebhook.DestinationURL
|
||||
var userID *uint
|
||||
var userName *string
|
||||
var userEmail *string
|
||||
activityType := activity.ActivityName()
|
||||
|
||||
if user != nil {
|
||||
// To support creating activities with users that were deleted. This can happen
|
||||
// for automatically installed software which uses the author of the upload as the author of
|
||||
// the installation.
|
||||
if user.ID != 0 && !user.Deleted {
|
||||
userID = &user.ID
|
||||
}
|
||||
userName = &user.Name
|
||||
userEmail = &user.Email
|
||||
} else if automatableActivity, ok := activity.(fleet.AutomatableActivity); ok && automatableActivity.WasFromAutomation() {
|
||||
userName = ptr.String(fleet.ActivityAutomationAuthor)
|
||||
func (svc *Service) NewActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
||||
var apiUser *activity_api.User
|
||||
if user != nil {
|
||||
apiUser = &activity_api.User{
|
||||
ID: user.ID,
|
||||
Name: user.Name,
|
||||
Email: user.Email,
|
||||
Deleted: user.Deleted,
|
||||
}
|
||||
|
||||
go func() {
|
||||
retryStrategy := backoff.NewExponentialBackOff()
|
||||
retryStrategy.MaxElapsedTime = 30 * time.Minute
|
||||
err := backoff.Retry(
|
||||
func() error {
|
||||
if err := server.PostJSONWithTimeout(
|
||||
context.Background(), webhookURL, &fleet.ActivityWebhookPayload{
|
||||
Timestamp: timestamp,
|
||||
ActorFullName: userName,
|
||||
ActorID: userID,
|
||||
ActorEmail: userEmail,
|
||||
Type: activityType,
|
||||
Details: (*json.RawMessage)(&detailsBytes),
|
||||
},
|
||||
); err != nil {
|
||||
var statusCoder kithttp.StatusCoder
|
||||
if errors.As(err, &statusCoder) && statusCoder.StatusCode() == http.StatusTooManyRequests {
|
||||
level.Debug(logger).Log("msg", "fire activity webhook", "err", err)
|
||||
return err
|
||||
}
|
||||
return backoff.Permanent(err)
|
||||
}
|
||||
return nil
|
||||
}, retryStrategy,
|
||||
)
|
||||
if err != nil {
|
||||
level.Error(logger).Log(
|
||||
"msg", fmt.Sprintf("fire activity webhook to %s", server.MaskSecretURLParams(webhookURL)), "err",
|
||||
server.MaskURLError(err).Error(),
|
||||
)
|
||||
}
|
||||
}()
|
||||
}
|
||||
// We update the context to indicate that we processed the webhook.
|
||||
ctx = context.WithValue(ctx, fleet.ActivityWebhookContextKey, true)
|
||||
return ds.NewActivity(ctx, user, activity, detailsBytes, timestamp)
|
||||
return svc.activitySvc.NewActivity(ctx, apiUser, activity)
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
|||
|
|
@ -3,11 +3,16 @@ package service
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
fleetserver "github.com/fleetdm/fleet/v4/server"
|
||||
"github.com/fleetdm/fleet/v4/server/activity"
|
||||
activity_api "github.com/fleetdm/fleet/v4/server/activity/api"
|
||||
activity_bootstrap "github.com/fleetdm/fleet/v4/server/activity/bootstrap"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mock"
|
||||
|
|
@ -16,6 +21,31 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// webhookTestProviders implements activity.DataProviders for webhook tests.
|
||||
type webhookTestProviders struct {
|
||||
getWebhookConfig func() (*activity.ActivitiesWebhookSettings, error)
|
||||
}
|
||||
|
||||
func (p *webhookTestProviders) GetActivitiesWebhookConfig(_ context.Context) (*activity.ActivitiesWebhookSettings, error) {
|
||||
return p.getWebhookConfig()
|
||||
}
|
||||
|
||||
func (p *webhookTestProviders) ActivateNextUpcomingActivity(_ context.Context, _ uint, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *webhookTestProviders) MaskSecretURLParams(rawURL string) string { return rawURL }
|
||||
func (p *webhookTestProviders) MaskURLError(err error) error { return err }
|
||||
func (p *webhookTestProviders) UsersByIDs(_ context.Context, _ []uint) ([]*activity.User, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (p *webhookTestProviders) FindUserIDs(_ context.Context, _ string) ([]uint, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (p *webhookTestProviders) GetHostLite(_ context.Context, _ uint) (*activity.Host, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type ActivityTypeTest struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
|
@ -82,11 +112,10 @@ func Test_logRoleChangeActivities(t *testing.T) {
|
|||
},
|
||||
}
|
||||
ds := new(mock.Store)
|
||||
svc, ctx := newTestService(t, ds, nil, nil)
|
||||
opts := &TestServerOpts{}
|
||||
svc, ctx := newTestService(t, ds, nil, nil, opts)
|
||||
var activities []string
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
opts.ActivityMock.NewActivityFunc = func(_ context.Context, _ *activity_api.User, activity activity_api.ActivityDetails) error {
|
||||
activities = append(activities, activity.ActivityName())
|
||||
return nil
|
||||
}
|
||||
|
|
@ -122,7 +151,8 @@ func Test_logRoleChangeActivities(t *testing.T) {
|
|||
|
||||
func TestActivityWebhooks(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
svc, ctx := newTestService(t, ds, nil, nil)
|
||||
opts := &TestServerOpts{}
|
||||
svc, ctx := newTestService(t, ds, nil, nil, opts)
|
||||
var webhookBody = fleet.ActivityWebhookPayload{}
|
||||
webhookChannel := make(chan struct{}, 1)
|
||||
fail429 := false
|
||||
|
|
@ -172,24 +202,23 @@ func TestActivityWebhooks(t *testing.T) {
|
|||
mockUrl := startMockServer(t)
|
||||
testUrl := mockUrl
|
||||
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{
|
||||
WebhookSettings: fleet.WebhookSettings{
|
||||
ActivitiesWebhook: fleet.ActivitiesWebhookSettings{
|
||||
Enable: true,
|
||||
DestinationURL: testUrl,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
// Wire a real activity bounded context service as delegate so that webhook
|
||||
// firing (which lives in the bounded context) is exercised. The mock still
|
||||
// captures invocations and the user for assertions.
|
||||
providers := &webhookTestProviders{
|
||||
getWebhookConfig: func() (*activity.ActivitiesWebhookSettings, error) {
|
||||
return &activity.ActivitiesWebhookSettings{
|
||||
Enable: true,
|
||||
DestinationURL: testUrl,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
var activityUser *fleet.User
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
realActivitySvc := activity_bootstrap.NewForUnitTests(providers, fleetserver.PostJSONWithTimeout, slog.New(slog.DiscardHandler))
|
||||
opts.ActivityMock.Delegate = realActivitySvc
|
||||
|
||||
var activityUser *activity_api.User
|
||||
opts.ActivityMock.NewActivityFunc = func(_ context.Context, user *activity_api.User, _ activity_api.ActivityDetails) error {
|
||||
activityUser = user
|
||||
assert.NotEmpty(t, details)
|
||||
assert.True(t, createdAt.After(time.Now().Add(-10*time.Second)))
|
||||
assert.False(t, createdAt.After(time.Now()))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -232,11 +261,11 @@ func TestActivityWebhooks(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
ds.NewActivityFuncInvoked = false
|
||||
opts.ActivityMock.NewActivityFuncInvoked = false
|
||||
testUrl = tt.url
|
||||
startTime := time.Now()
|
||||
activity := ActivityTypeTest{Name: tt.name}
|
||||
err := svc.NewActivity(ctx, tt.user, activity)
|
||||
act := ActivityTypeTest{Name: tt.name}
|
||||
err := svc.NewActivity(ctx, tt.user, act)
|
||||
require.NoError(t, err)
|
||||
select {
|
||||
case <-time.After(1 * time.Second):
|
||||
|
|
@ -263,15 +292,22 @@ func TestActivityWebhooks(t *testing.T) {
|
|||
require.NotNil(t, webhookBody.ActorEmail)
|
||||
assert.Equal(t, tt.user.Email, *webhookBody.ActorEmail)
|
||||
}
|
||||
assert.Equal(t, activity.ActivityName(), webhookBody.Type)
|
||||
assert.Equal(t, act.ActivityName(), webhookBody.Type)
|
||||
var details map[string]string
|
||||
require.NoError(t, json.Unmarshal(*webhookBody.Details, &details))
|
||||
assert.Len(t, details, 1)
|
||||
assert.Equal(t, tt.name, details["name"])
|
||||
}
|
||||
}
|
||||
require.True(t, ds.NewActivityFuncInvoked)
|
||||
assert.Equal(t, tt.user, activityUser)
|
||||
require.True(t, opts.ActivityMock.NewActivityFuncInvoked)
|
||||
if tt.user == nil {
|
||||
assert.Nil(t, activityUser)
|
||||
} else {
|
||||
require.NotNil(t, activityUser)
|
||||
assert.Equal(t, tt.user.ID, activityUser.ID)
|
||||
assert.Equal(t, tt.user.Name, activityUser.Name)
|
||||
assert.Equal(t, tt.user.Email, activityUser.Email)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -279,7 +315,8 @@ func TestActivityWebhooks(t *testing.T) {
|
|||
|
||||
func TestActivityWebhooksDisabled(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
svc, ctx := newTestService(t, ds, nil, nil)
|
||||
opts := &TestServerOpts{}
|
||||
svc, ctx := newTestService(t, ds, nil, nil, opts)
|
||||
startMockServer := func(t *testing.T) string {
|
||||
// create a test http server
|
||||
srv := httptest.NewServer(
|
||||
|
|
@ -304,14 +341,9 @@ func TestActivityWebhooksDisabled(t *testing.T) {
|
|||
},
|
||||
}, nil
|
||||
}
|
||||
var activityUser *fleet.User
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
var activityUser *activity_api.User
|
||||
opts.ActivityMock.NewActivityFunc = func(_ context.Context, user *activity_api.User, _ activity_api.ActivityDetails) error {
|
||||
activityUser = user
|
||||
assert.NotEmpty(t, details)
|
||||
assert.True(t, createdAt.After(time.Now().Add(-10*time.Second)))
|
||||
assert.False(t, createdAt.After(time.Now()))
|
||||
return nil
|
||||
}
|
||||
activity := ActivityTypeTest{Name: "no webhook"}
|
||||
|
|
@ -321,8 +353,11 @@ func TestActivityWebhooksDisabled(t *testing.T) {
|
|||
Email: "testUser@example.com",
|
||||
}
|
||||
require.NoError(t, svc.NewActivity(ctx, user, activity))
|
||||
require.True(t, ds.NewActivityFuncInvoked)
|
||||
assert.Equal(t, user, activityUser)
|
||||
require.True(t, opts.ActivityMock.NewActivityFuncInvoked)
|
||||
require.NotNil(t, activityUser)
|
||||
assert.Equal(t, user.ID, activityUser.ID)
|
||||
assert.Equal(t, user.Name, activityUser.Name)
|
||||
assert.Equal(t, user.Email, activityUser.Email)
|
||||
}
|
||||
|
||||
func TestCancelHostUpcomingActivityAuth(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/optjson"
|
||||
"github.com/fleetdm/fleet/v4/server/config"
|
||||
|
|
@ -227,9 +226,6 @@ func TestEnrollSecretAuth(t *testing.T) {
|
|||
return nil, nil
|
||||
}
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { return &fleet.AppConfig{}, nil }
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
|
@ -342,9 +338,6 @@ func TestApplyEnrollSecretWithGlobalEnrollConfig(t *testing.T) {
|
|||
return nil, nil
|
||||
}
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { return &fleet.AppConfig{}, nil }
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
ds.IsEnrollSecretAvailableFunc = nil
|
||||
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
|
||||
|
|
@ -1480,11 +1473,6 @@ func TestModifyAppConfigSMTPSSOAgentOptions(t *testing.T) {
|
|||
*dsAppConfig = *conf
|
||||
return nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3322,6 +3322,7 @@ type MDMAppleCheckinAndCommandService struct {
|
|||
mdmLifecycle *mdmlifecycle.HostLifecycle
|
||||
commandHandlers map[string][]fleet.MDMCommandResultsHandler
|
||||
keyValueStore fleet.KeyValueStore
|
||||
newActivityFn mdmlifecycle.NewActivityFunc
|
||||
isPremium bool
|
||||
}
|
||||
|
||||
|
|
@ -3332,8 +3333,9 @@ func NewMDMAppleCheckinAndCommandService(
|
|||
isPremium bool,
|
||||
logger *platformlogging.Logger,
|
||||
keyValueStore fleet.KeyValueStore,
|
||||
newActivityFn mdmlifecycle.NewActivityFunc,
|
||||
) *MDMAppleCheckinAndCommandService {
|
||||
mdmLifecycle := mdmlifecycle.New(ds, logger, newActivity)
|
||||
mdmLifecycle := mdmlifecycle.New(ds, logger, newActivityFn)
|
||||
return &MDMAppleCheckinAndCommandService{
|
||||
ds: ds,
|
||||
commander: commander,
|
||||
|
|
@ -3343,6 +3345,7 @@ func NewMDMAppleCheckinAndCommandService(
|
|||
isPremium: isPremium,
|
||||
commandHandlers: map[string][]fleet.MDMCommandResultsHandler{},
|
||||
keyValueStore: keyValueStore,
|
||||
newActivityFn: newActivityFn,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3548,13 +3551,13 @@ func (svc *MDMAppleCheckinAndCommandService) CheckOut(r *mdm.Request, m *mdm.Che
|
|||
return err
|
||||
}
|
||||
|
||||
return newActivity(
|
||||
return svc.newActivityFn(
|
||||
r.Context, nil, &fleet.ActivityTypeMDMUnenrolled{
|
||||
HostSerial: info.HardwareSerial,
|
||||
HostDisplayName: info.DisplayName,
|
||||
InstalledFromDEP: info.InstalledFromDEP,
|
||||
Platform: info.Platform,
|
||||
}, svc.ds, svc.logger,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -3814,7 +3817,7 @@ func (svc *MDMAppleCheckinAndCommandService) CommandAndReportResults(r *mdm.Requ
|
|||
return nil, ctxerr.Wrap(r.Context, err, "fetching data for installed app store app activity")
|
||||
}
|
||||
act.FromSetupExperience = fromSetupExperience
|
||||
if err := newActivity(r.Context, user, act, svc.ds, svc.logger); err != nil {
|
||||
if err := svc.newActivityFn(r.Context, user, act); err != nil {
|
||||
return nil, ctxerr.Wrap(r.Context, err, "creating activity for installed app store app")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
||||
mdmlifecycle "github.com/fleetdm/fleet/v4/server/mdm/lifecycle"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
||||
"github.com/fleetdm/fleet/v4/server/platform/logging"
|
||||
"github.com/fleetdm/fleet/v4/server/worker"
|
||||
|
|
@ -64,6 +65,7 @@ func NewInstalledApplicationListResultsHandler(
|
|||
commander *apple_mdm.MDMAppleCommander,
|
||||
logger *logging.Logger,
|
||||
verifyTimeout, verifyRequestDelay time.Duration,
|
||||
newActivityFn mdmlifecycle.NewActivityFunc,
|
||||
) fleet.MDMCommandResultsHandler {
|
||||
return func(ctx context.Context, commandResults fleet.MDMCommandResults) error {
|
||||
installedAppResult, ok := commandResults.(InstalledApplicationListResult)
|
||||
|
|
@ -202,7 +204,7 @@ func NewInstalledApplicationListResultsHandler(
|
|||
return ctxerr.Wrap(ctx, err, "fetching data for installed app store app activity")
|
||||
}
|
||||
|
||||
if err := newActivity(ctx, user, act, ds, logger); err != nil {
|
||||
if err := newActivityFn(ctx, user, act); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "creating activity for installed app store app")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ func (nopProfileMatcher) RetrieveProfiles(ctx context.Context, extHostID string)
|
|||
return fleet.MDMApplePreassignHostProfiles{}, nil
|
||||
}
|
||||
|
||||
func setupAppleMDMService(t *testing.T, license *fleet.LicenseInfo) (fleet.Service, context.Context, *mock.Store) {
|
||||
func setupAppleMDMService(t *testing.T, license *fleet.LicenseInfo) (fleet.Service, context.Context, *mock.Store, *TestServerOpts) {
|
||||
ds := new(mock.Store)
|
||||
cfg := config.TestConfig()
|
||||
testCertPEM, testKeyPEM, err := generateCertWithAPNsTopic()
|
||||
|
|
@ -253,11 +253,11 @@ func setupAppleMDMService(t *testing.T, license *fleet.LicenseInfo) (fleet.Servi
|
|||
return []*fleet.ABMToken{{ID: 1}}, nil
|
||||
}
|
||||
|
||||
return svc, ctx, ds
|
||||
return svc, ctx, ds, opts
|
||||
}
|
||||
|
||||
func TestAppleMDMAuthorization(t *testing.T) {
|
||||
svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
||||
svc, ctx, ds, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
||||
|
||||
ds.GetEnrollSecretsFunc = func(ctx context.Context, teamID *uint) ([]*fleet.EnrollSecret, error) {
|
||||
return []*fleet.EnrollSecret{
|
||||
|
|
@ -580,7 +580,7 @@ func TestAppleMDMAuthorization(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMDMAppleConfigProfileAuthz(t *testing.T) {
|
||||
svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
||||
svc, ctx, ds, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
||||
|
||||
profUUID := "a" + uuid.NewString()
|
||||
testCases := []struct {
|
||||
|
|
@ -657,9 +657,6 @@ func TestMDMAppleConfigProfileAuthz(t *testing.T) {
|
|||
ds.ListMDMAppleConfigProfilesFunc = func(ctx context.Context, teamID *uint) ([]*fleet.MDMAppleConfigProfile, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(context.Context, *fleet.User, fleet.ActivityDetails, []byte, time.Time) error {
|
||||
return nil
|
||||
}
|
||||
ds.GetMDMAppleProfilesSummaryFunc = func(context.Context, *uint) (*fleet.MDMProfilesSummary, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
|
@ -772,7 +769,7 @@ func TestMDMAppleConfigProfileAuthz(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNewMDMAppleConfigProfile(t *testing.T) {
|
||||
svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
||||
svc, ctx, ds, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
||||
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
||||
|
||||
identifier := "Bar.$FLEET_VAR_HOST_END_USER_EMAIL_IDP"
|
||||
|
|
@ -784,9 +781,6 @@ func TestNewMDMAppleConfigProfile(t *testing.T) {
|
|||
require.Equal(t, mcBytes, []byte(cp.Mobileconfig))
|
||||
return &cp, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(context.Context, *fleet.User, fleet.ActivityDetails, []byte, time.Time) error {
|
||||
return nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
|
|
@ -835,7 +829,7 @@ func mcBytesForTest(name, identifier, uuid string) []byte {
|
|||
}
|
||||
|
||||
func TestBatchSetMDMAppleProfilesWithSecrets(t *testing.T) {
|
||||
svc, ctx, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
||||
svc, ctx, _, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
||||
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
||||
|
||||
// Test profile with FLEET_SECRET in PayloadDisplayName
|
||||
|
|
@ -852,7 +846,7 @@ func TestBatchSetMDMAppleProfilesWithSecrets(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNewMDMAppleDeclaration(t *testing.T) {
|
||||
svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
||||
svc, ctx, ds, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
||||
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
||||
|
||||
// Unsupported Fleet variable
|
||||
|
|
@ -868,9 +862,6 @@ func TestNewMDMAppleDeclaration(t *testing.T) {
|
|||
ds.NewMDMAppleDeclarationFunc = func(ctx context.Context, d *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
|
||||
return d, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(context.Context, *fleet.User, fleet.ActivityDetails, []byte, time.Time) error {
|
||||
return nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
|
|
@ -885,7 +876,7 @@ func TestNewMDMAppleDeclaration(t *testing.T) {
|
|||
|
||||
// Fragile test: This test is fragile because of the large reliance on Datastore mocks. Consider refactoring test/logic or removing the test. It may be slowing us down more than helping us.
|
||||
func TestHostDetailsMDMProfiles(t *testing.T) {
|
||||
svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
||||
svc, ctx, ds, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
||||
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
||||
|
||||
expected := []fleet.HostMDMAppleProfile{
|
||||
|
|
@ -1070,7 +1061,7 @@ func TestHostDetailsMDMProfiles(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMDMCommandAuthz(t *testing.T) {
|
||||
svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
||||
svc, ctx, ds, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
||||
|
||||
ds.HostLiteFunc = func(ctx context.Context, hostID uint) (*fleet.Host, error) {
|
||||
switch hostID {
|
||||
|
|
@ -1085,9 +1076,6 @@ func TestMDMCommandAuthz(t *testing.T) {
|
|||
return &fleet.HostMDMCheckinInfo{Platform: "darwin"}, nil
|
||||
}
|
||||
|
||||
ds.NewActivityFunc = func(context.Context, *fleet.User, fleet.ActivityDetails, []byte, time.Time) error {
|
||||
return nil
|
||||
}
|
||||
ds.MDMTurnOffFunc = func(ctx context.Context, uuid string) ([]*fleet.User, []fleet.ActivityDetails, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
|
@ -1202,7 +1190,7 @@ func TestMDMCommandAuthz(t *testing.T) {
|
|||
|
||||
func TestMDMAuthenticateManualEnrollment(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
mdmLifecycle := mdmlifecycle.New(ds, logging.NewNopLogger(), newActivity)
|
||||
mdmLifecycle := mdmlifecycle.New(ds, logging.NewNopLogger(), func(_ context.Context, _ *fleet.User, _ fleet.ActivityDetails) error { return nil })
|
||||
svc := MDMAppleCheckinAndCommandService{
|
||||
ds: ds,
|
||||
mdmLifecycle: mdmLifecycle,
|
||||
|
|
@ -1252,7 +1240,7 @@ func TestMDMAuthenticateManualEnrollment(t *testing.T) {
|
|||
|
||||
func TestMDMAuthenticateADE(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
mdmLifecycle := mdmlifecycle.New(ds, logging.NewNopLogger(), newActivity)
|
||||
mdmLifecycle := mdmlifecycle.New(ds, logging.NewNopLogger(), func(_ context.Context, _ *fleet.User, _ fleet.ActivityDetails) error { return nil })
|
||||
svc := MDMAppleCheckinAndCommandService{
|
||||
ds: ds,
|
||||
mdmLifecycle: mdmLifecycle,
|
||||
|
|
@ -1302,7 +1290,11 @@ func TestMDMAuthenticateADE(t *testing.T) {
|
|||
|
||||
func TestMDMAuthenticateSCEPRenewal(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
mdmLifecycle := mdmlifecycle.New(ds, logging.NewNopLogger(), newActivity)
|
||||
var newActivityInvoked bool
|
||||
mdmLifecycle := mdmlifecycle.New(ds, logging.NewNopLogger(), func(_ context.Context, _ *fleet.User, _ fleet.ActivityDetails) error {
|
||||
newActivityInvoked = true
|
||||
return nil
|
||||
})
|
||||
svc := MDMAppleCheckinAndCommandService{
|
||||
ds: ds,
|
||||
mdmLifecycle: mdmLifecycle,
|
||||
|
|
@ -1320,11 +1312,6 @@ func TestMDMAuthenticateSCEPRenewal(t *testing.T) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.MDMResetEnrollmentFunc = func(ctx context.Context, hostUUID string, scepRenewalInProgress bool) error {
|
||||
require.Equal(t, uuid, hostUUID)
|
||||
require.True(t, scepRenewalInProgress)
|
||||
|
|
@ -1351,12 +1338,12 @@ func TestMDMAuthenticateSCEPRenewal(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
require.False(t, ds.MDMAppleUpsertHostFuncInvoked)
|
||||
require.True(t, ds.GetHostMDMCheckinInfoFuncInvoked)
|
||||
require.False(t, ds.NewActivityFuncInvoked)
|
||||
require.False(t, newActivityInvoked)
|
||||
require.True(t, ds.MDMResetEnrollmentFuncInvoked)
|
||||
}
|
||||
|
||||
func TestAppleMDMUnenrollment(t *testing.T) {
|
||||
svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
||||
svc, ctx, ds, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
||||
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{ID: 1, GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
||||
|
||||
hostOne := &fleet.Host{ID: 1, UUID: "test-host-no-team-2", Platform: "ios"}
|
||||
|
|
@ -1377,9 +1364,6 @@ func TestAppleMDMUnenrollment(t *testing.T) {
|
|||
return &fleet.HostMDMCheckinInfo{Platform: "darwin"}, nil
|
||||
}
|
||||
|
||||
ds.NewActivityFunc = func(context.Context, *fleet.User, fleet.ActivityDetails, []byte, time.Time) error {
|
||||
return nil
|
||||
}
|
||||
ds.MDMTurnOffFunc = func(ctx context.Context, uuid string) ([]*fleet.User, []fleet.ActivityDetails, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
|
@ -1419,14 +1403,28 @@ func TestMDMTokenUpdate(t *testing.T) {
|
|||
NewNanoMDMLogger(logging.NewJSONLogger(os.Stdout)),
|
||||
)
|
||||
cmdr := apple_mdm.NewMDMAppleCommander(mdmStorage, pusher)
|
||||
mdmLifecycle := mdmlifecycle.New(ds, logging.NewNopLogger(), newActivity)
|
||||
uuid, serial, model, wantTeamID := "ABC-DEF-GHI", "XYZABC", "MacBookPro 16,1", uint(12)
|
||||
var newActivityFuncInvoked bool
|
||||
mdmLifecycle := mdmlifecycle.New(ds, logging.NewNopLogger(), func(_ context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
||||
newActivityFuncInvoked = true
|
||||
a, ok := activity.(*fleet.ActivityTypeMDMEnrolled)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, user)
|
||||
require.Equal(t, "mdm_enrolled", activity.ActivityName())
|
||||
require.NotNil(t, a.HostSerial)
|
||||
require.Equal(t, serial, *a.HostSerial)
|
||||
require.Nil(t, a.EnrollmentID)
|
||||
require.Equal(t, a.HostDisplayName, model)
|
||||
require.True(t, a.InstalledFromDEP)
|
||||
require.Equal(t, fleet.MDMPlatformApple, a.MDMPlatform)
|
||||
return nil
|
||||
})
|
||||
svc := MDMAppleCheckinAndCommandService{
|
||||
ds: ds,
|
||||
mdmLifecycle: mdmLifecycle,
|
||||
commander: cmdr,
|
||||
logger: logging.NewNopLogger(),
|
||||
}
|
||||
uuid, serial, model, wantTeamID := "ABC-DEF-GHI", "XYZABC", "MacBookPro 16,1", uint(12)
|
||||
|
||||
ds.AppConfigFunc = func(context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
|
|
@ -1459,22 +1457,6 @@ func TestMDMTokenUpdate(t *testing.T) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
a, ok := activity.(*fleet.ActivityTypeMDMEnrolled)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, user)
|
||||
require.Equal(t, "mdm_enrolled", activity.ActivityName())
|
||||
require.NotNil(t, a.HostSerial)
|
||||
require.Equal(t, serial, *a.HostSerial)
|
||||
require.Nil(t, a.EnrollmentID)
|
||||
require.Equal(t, a.HostDisplayName, model)
|
||||
require.True(t, a.InstalledFromDEP)
|
||||
require.Equal(t, fleet.MDMPlatformApple, a.MDMPlatform)
|
||||
return nil
|
||||
}
|
||||
|
||||
ds.NewJobFunc = func(ctx context.Context, j *fleet.Job) (*fleet.Job, error) {
|
||||
return j, nil
|
||||
}
|
||||
|
|
@ -1490,7 +1472,7 @@ func TestMDMTokenUpdate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
require.True(t, ds.GetHostMDMCheckinInfoFuncInvoked)
|
||||
require.True(t, ds.NewJobFuncInvoked)
|
||||
require.True(t, ds.NewActivityFuncInvoked)
|
||||
require.True(t, newActivityFuncInvoked)
|
||||
ds.GetHostMDMCheckinInfoFuncInvoked = false
|
||||
ds.NewJobFuncInvoked = false
|
||||
|
||||
|
|
@ -1510,7 +1492,7 @@ func TestMDMTokenUpdate(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
require.True(t, ds.GetHostMDMCheckinInfoFuncInvoked)
|
||||
require.True(t, ds.NewJobFuncInvoked)
|
||||
require.True(t, ds.NewActivityFuncInvoked)
|
||||
require.True(t, newActivityFuncInvoked)
|
||||
|
||||
// With AwaitingConfiguration - should check for and enqueue SetupExperience items
|
||||
ds.EnqueueSetupExperienceItemsFunc = func(ctx context.Context, hostPlatformLike string, hostUUID string, teamID uint) (bool, error) {
|
||||
|
|
@ -1573,7 +1555,7 @@ func TestMDMTokenUpdate(t *testing.T) {
|
|||
// Should NOT call the setup experience enqueue function but it should mark the migration complete
|
||||
require.False(t, ds.EnqueueSetupExperienceItemsFuncInvoked)
|
||||
require.True(t, ds.SetHostMDMMigrationCompletedFuncInvoked)
|
||||
require.True(t, ds.NewActivityFuncInvoked)
|
||||
require.True(t, newActivityFuncInvoked)
|
||||
|
||||
ds.SetHostMDMMigrationCompletedFuncInvoked = false
|
||||
err = svc.TokenUpdate(
|
||||
|
|
@ -1592,7 +1574,7 @@ func TestMDMTokenUpdate(t *testing.T) {
|
|||
// Should NOT call the setup experience enqueue function but it should mark the migration complete
|
||||
require.False(t, ds.EnqueueSetupExperienceItemsFuncInvoked)
|
||||
require.True(t, ds.SetHostMDMMigrationCompletedFuncInvoked)
|
||||
require.True(t, ds.NewActivityFuncInvoked)
|
||||
require.True(t, newActivityFuncInvoked)
|
||||
}
|
||||
|
||||
func TestMDMTokenUpdateIOS(t *testing.T) {
|
||||
|
|
@ -1607,7 +1589,7 @@ func TestMDMTokenUpdateIOS(t *testing.T) {
|
|||
NewNanoMDMLogger(logging.NewJSONLogger(os.Stdout)),
|
||||
)
|
||||
cmdr := apple_mdm.NewMDMAppleCommander(mdmStorage, pusher)
|
||||
mdmLifecycle := mdmlifecycle.New(ds, logging.NewNopLogger(), newActivity)
|
||||
mdmLifecycle := mdmlifecycle.New(ds, logging.NewNopLogger(), func(_ context.Context, _ *fleet.User, _ fleet.ActivityDetails) error { return nil })
|
||||
svc := MDMAppleCheckinAndCommandService{
|
||||
ds: ds,
|
||||
mdmLifecycle: mdmLifecycle,
|
||||
|
|
@ -1630,10 +1612,6 @@ func TestMDMTokenUpdateIOS(t *testing.T) {
|
|||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
ds.NewJobFunc = func(ctx context.Context, j *fleet.Job) (*fleet.Job, error) {
|
||||
return j, nil
|
||||
}
|
||||
|
|
@ -1763,7 +1741,8 @@ func TestMDMTokenUpdateIOS(t *testing.T) {
|
|||
|
||||
func TestMDMCheckout(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
mdmLifecycle := mdmlifecycle.New(ds, logging.NewNopLogger(), newActivity)
|
||||
mdmLifecycle := mdmlifecycle.New(ds, logging.NewNopLogger(), func(_ context.Context, _ *fleet.User, _ fleet.ActivityDetails) error { return nil })
|
||||
var newActivityFuncInvoked bool
|
||||
svc := MDMAppleCheckinAndCommandService{
|
||||
ds: ds,
|
||||
mdmLifecycle: mdmLifecycle,
|
||||
|
|
@ -1790,9 +1769,10 @@ func TestMDMCheckout(t *testing.T) {
|
|||
ds.AppConfigFunc = func(context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
svc.newActivityFn = func(
|
||||
_ context.Context, user *fleet.User, activity fleet.ActivityDetails,
|
||||
) error {
|
||||
newActivityFuncInvoked = true
|
||||
a, ok := activity.(*fleet.ActivityTypeMDMUnenrolled)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, user)
|
||||
|
|
@ -1818,7 +1798,7 @@ func TestMDMCheckout(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
require.True(t, ds.MDMTurnOffFuncInvoked)
|
||||
require.True(t, ds.GetHostMDMCheckinInfoFuncInvoked)
|
||||
require.True(t, ds.NewActivityFuncInvoked)
|
||||
require.True(t, newActivityFuncInvoked)
|
||||
}
|
||||
|
||||
func TestMDMCommandAndReportResultsProfileHandling(t *testing.T) {
|
||||
|
|
@ -1961,7 +1941,7 @@ func TestMDMCommandAndReportResultsProfileHandling(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMDMBatchSetAppleProfiles(t *testing.T) {
|
||||
svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
||||
svc, ctx, ds, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
||||
|
||||
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
|
||||
return &fleet.Team{ID: 1, Name: name}, nil
|
||||
|
|
@ -1972,11 +1952,6 @@ func TestMDMBatchSetAppleProfiles(t *testing.T) {
|
|||
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, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
|
|
@ -2309,7 +2284,7 @@ func TestMDMBatchSetAppleProfiles(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMDMBatchSetAppleProfilesBoolArgs(t *testing.T) {
|
||||
svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
||||
svc, ctx, ds, svcOpts := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
||||
|
||||
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
|
||||
return &fleet.Team{ID: 1, Name: name}, nil
|
||||
|
|
@ -2320,11 +2295,6 @@ func TestMDMBatchSetAppleProfilesBoolArgs(t *testing.T) {
|
|||
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, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, profileUUIDs, uuids []string,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
|
|
@ -2341,18 +2311,18 @@ func TestMDMBatchSetAppleProfilesBoolArgs(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
require.False(t, ds.BatchSetMDMAppleProfilesFuncInvoked)
|
||||
require.False(t, ds.BulkSetPendingMDMHostProfilesFuncInvoked)
|
||||
require.False(t, ds.NewActivityFuncInvoked)
|
||||
require.False(t, svcOpts.ActivityMock.NewActivityFuncInvoked)
|
||||
|
||||
// skipping bulk set only skips that method
|
||||
err = svc.BatchSetMDMAppleProfiles(ctx, nil, nil, [][]byte{}, false, true)
|
||||
require.NoError(t, err)
|
||||
require.True(t, ds.BatchSetMDMAppleProfilesFuncInvoked)
|
||||
require.False(t, ds.BulkSetPendingMDMHostProfilesFuncInvoked)
|
||||
require.True(t, ds.NewActivityFuncInvoked)
|
||||
require.True(t, svcOpts.ActivityMock.NewActivityFuncInvoked)
|
||||
}
|
||||
|
||||
func TestUpdateMDMAppleSettings(t *testing.T) {
|
||||
svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
||||
svc, ctx, ds, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
||||
|
||||
ds.TeamWithExtrasFunc = func(ctx context.Context, id uint) (*fleet.Team, error) {
|
||||
return &fleet.Team{ID: id, Name: "team"}, nil
|
||||
|
|
@ -2360,11 +2330,6 @@ func TestUpdateMDMAppleSettings(t *testing.T) {
|
|||
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
||||
return team, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
|
|
@ -2509,18 +2474,13 @@ func TestUpdateMDMAppleSettings(t *testing.T) {
|
|||
|
||||
func TestUpdateMDMAppleSetup(t *testing.T) {
|
||||
setupTest := func(tier string) (fleet.Service, context.Context, *mock.Store) {
|
||||
svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: tier})
|
||||
svc, ctx, ds, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: tier})
|
||||
ds.TeamWithExtrasFunc = func(ctx context.Context, id uint) (*fleet.Team, error) {
|
||||
return &fleet.Team{ID: id, Name: "team"}, nil
|
||||
}
|
||||
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
||||
return team, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}, nil
|
||||
}
|
||||
|
|
@ -4109,13 +4069,8 @@ func TestEnsureFleetdConfig(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMDMAppleSetupAssistant(t *testing.T) {
|
||||
svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
||||
svc, ctx, ds, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
||||
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.NewJobFunc = func(ctx context.Context, j *fleet.Job) (*fleet.Job, error) {
|
||||
return j, nil
|
||||
}
|
||||
|
|
@ -4199,7 +4154,7 @@ func TestMDMAppleSetupAssistant(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMDMApplePreassignEndpoints(t *testing.T) {
|
||||
svc, ctx, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
||||
svc, ctx, _, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
||||
|
||||
checkAuthErr := func(t *testing.T, err error, shouldFailWithAuth bool) {
|
||||
t.Helper()
|
||||
|
|
@ -5118,7 +5073,7 @@ func TestNeedsOSUpdateForDEPEnrollment(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCheckMDMAppleEnrollmentWithMinimumOSVersion(t *testing.T) {
|
||||
svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
||||
svc, ctx, ds, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
||||
|
||||
gdmf := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import (
|
|||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
|
|
@ -64,10 +63,6 @@ func TestCreateCertificateTemplate(t *testing.T) {
|
|||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
t.Run("Invalid CA type", func(t *testing.T) {
|
||||
_, err := svc.CreateCertificateTemplate(ctx, "my template", TeamID, uint(InvalidCATypeID), "CN=$FLEET_VAR_HOST_UUID")
|
||||
require.Error(t, err)
|
||||
|
|
@ -193,10 +188,6 @@ func TestApplyCertificateTemplateSpecs(t *testing.T) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set up certificate authority mocks
|
||||
certAuthorities := []*fleet.CertificateAuthority{
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package service
|
|||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
|
|
@ -66,11 +65,6 @@ func TestGlobalPoliciesAuth(t *testing.T) {
|
|||
ds.ApplyPolicySpecsFunc = func(ctx context.Context, authorID uint, specs []*fleet.PolicySpec) error {
|
||||
return nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.SavePolicyFunc = func(ctx context.Context, p *fleet.Policy, shouldDeleteAll bool, removePolicyStats bool) error {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -266,11 +260,6 @@ func TestApplyPolicySpecsLabelsValidation(t *testing.T) {
|
|||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.ApplyPolicySpecsFunc = func(ctx context.Context, authorID uint, specs []*fleet.PolicySpec) error {
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package service
|
|||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
|
|
@ -31,11 +30,6 @@ func TestGlobalScheduleAuth(t *testing.T) {
|
|||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) {
|
||||
return nil, 0, 0, nil, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -585,7 +585,7 @@ func (svc *Service) DeleteHosts(ctx context.Context, ids []uint, filter *map[str
|
|||
return err
|
||||
}
|
||||
|
||||
mdmLifecycle := mdmlifecycle.New(svc.ds, svc.logger, newActivity)
|
||||
mdmLifecycle := mdmlifecycle.New(svc.ds, svc.logger, svc.NewActivity)
|
||||
lifecycleErrs := []error{}
|
||||
serialsWithErrs := []string{}
|
||||
for _, host := range hosts {
|
||||
|
|
@ -1104,7 +1104,7 @@ func (svc *Service) DeleteHost(ctx context.Context, id uint) error {
|
|||
}
|
||||
|
||||
if fleet.MDMSupported(host.Platform) {
|
||||
mdmLifecycle := mdmlifecycle.New(svc.ds, svc.logger, newActivity)
|
||||
mdmLifecycle := mdmlifecycle.New(svc.ds, svc.logger, svc.NewActivity)
|
||||
err = mdmLifecycle.Do(ctx, mdmlifecycle.HostOptions{
|
||||
Action: mdmlifecycle.HostActionDelete,
|
||||
Platform: host.Platform,
|
||||
|
|
|
|||
|
|
@ -751,9 +751,6 @@ func TestHostAuth(t *testing.T) {
|
|||
ds.DeleteHostFunc = func(ctx context.Context, hid uint) error {
|
||||
return nil
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
|
||||
if id == 1 {
|
||||
return teamHost, nil
|
||||
|
|
@ -820,9 +817,6 @@ func TestHostAuth(t *testing.T) {
|
|||
ds.TeamLiteFunc = func(ctx context.Context, id uint) (*fleet.TeamLite, error) {
|
||||
return &fleet.TeamLite{ID: id}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, u *fleet.User, a fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
ds.ListHostsLiteByIDsFunc = func(ctx context.Context, ids []uint) ([]*fleet.Host, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
|
@ -1371,6 +1365,7 @@ func TestDeleteHostCreatesActivity(t *testing.T) {
|
|||
activitySvc := mysql.NewTestActivityService(t, ds)
|
||||
|
||||
svc, ctx := newTestService(t, ds, nil, nil)
|
||||
svc.SetActivityService(activitySvc)
|
||||
|
||||
// Create a user for the deletion
|
||||
user := &fleet.User{
|
||||
|
|
@ -1426,6 +1421,7 @@ func TestDeleteHostsCreatesActivities(t *testing.T) {
|
|||
activitySvc := mysql.NewTestActivityService(t, ds)
|
||||
|
||||
svc, ctx := newTestService(t, ds, nil, nil)
|
||||
svc.SetActivityService(activitySvc)
|
||||
|
||||
// Create a user for the deletion
|
||||
user := &fleet.User{
|
||||
|
|
@ -1494,6 +1490,7 @@ func TestCleanupExpiredHostsActivities(t *testing.T) {
|
|||
activitySvc := mysql.NewTestActivityService(t, ds)
|
||||
|
||||
svc, ctx := newTestService(t, ds, nil, nil)
|
||||
svc.SetActivityService(activitySvc)
|
||||
|
||||
// Set global host expiry
|
||||
const globalExpiryWindow = 10
|
||||
|
|
@ -1680,11 +1677,6 @@ func TestAddHostsToTeamByFilter(t *testing.T) {
|
|||
ds.ListMDMAppleDEPSerialsInHostIDsFunc = func(ctx context.Context, hids []uint) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
emptyRequest := &map[string]interface{}{}
|
||||
|
||||
|
|
@ -1726,11 +1718,6 @@ func TestAddHostsToTeamByFilterLabel(t *testing.T) {
|
|||
ds.TeamLiteFunc = func(ctx context.Context, id uint) (*fleet.TeamLite, error) {
|
||||
return &fleet.TeamLite{ID: id}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
filter := &map[string]interface{}{"label_id": expectedLabel}
|
||||
|
||||
|
|
@ -1778,9 +1765,6 @@ func TestAddHostsToTeamSourceTeamAuth(t *testing.T) {
|
|||
ds.ListMDMAppleDEPSerialsInHostIDsFunc = func(ctx context.Context, hids []uint) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
ds.TeamLiteFunc = func(ctx context.Context, id uint) (*fleet.TeamLite, error) {
|
||||
return &fleet.TeamLite{ID: id}, nil
|
||||
}
|
||||
|
|
@ -1951,9 +1935,6 @@ func TestAddHostsToTeamByFilterSourceTeamAuth(t *testing.T) {
|
|||
ds.ListMDMAppleDEPSerialsInHostIDsFunc = func(ctx context.Context, hids []uint) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
ds.TeamLiteFunc = func(ctx context.Context, id uint) (*fleet.TeamLite, error) {
|
||||
return &fleet.TeamLite{ID: id}, nil
|
||||
}
|
||||
|
|
@ -2384,7 +2365,8 @@ func TestHostEncryptionKey(t *testing.T) {
|
|||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}, nil
|
||||
}
|
||||
svc, ctx := newTestServiceWithConfig(t, ds, fleetCfg, nil, nil)
|
||||
opts := &TestServerOpts{}
|
||||
svc, ctx := newTestServiceWithConfig(t, ds, fleetCfg, nil, nil, opts)
|
||||
|
||||
ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
|
||||
require.Equal(t, tt.host.ID, id)
|
||||
|
|
@ -2401,9 +2383,7 @@ func TestHostEncryptionKey(t *testing.T) {
|
|||
return &fleet.HostArchivedDiskEncryptionKey{}, nil
|
||||
}
|
||||
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
opts.ActivityMock.NewActivityFunc = func(_ context.Context, _ *activity_api.User, activity activity_api.ActivityDetails) error {
|
||||
act := activity.(fleet.ActivityTypeReadHostDiskEncryptionKey)
|
||||
require.Equal(t, tt.host.ID, act.HostID)
|
||||
require.Equal(t, []uint{tt.host.ID}, act.HostIDs())
|
||||
|
|
@ -2448,7 +2428,8 @@ func TestHostEncryptionKey(t *testing.T) {
|
|||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}, nil
|
||||
}
|
||||
svc, ctx := newTestServiceWithConfig(t, ds, fleetCfg, nil, nil)
|
||||
opts := &TestServerOpts{}
|
||||
svc, ctx := newTestServiceWithConfig(t, ds, fleetCfg, nil, nil, opts)
|
||||
ctx = test.UserContext(ctx, test.UserAdmin)
|
||||
|
||||
hostErr := errors.New("host error")
|
||||
|
|
@ -2485,9 +2466,7 @@ func TestHostEncryptionKey(t *testing.T) {
|
|||
return &fleet.HostDiskEncryptionKey{Base64Encrypted: "key"}, nil
|
||||
}
|
||||
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
opts.ActivityMock.NewActivityFunc = func(_ context.Context, _ *activity_api.User, _ activity_api.ActivityDetails) error {
|
||||
return errors.New("activity error")
|
||||
}
|
||||
|
||||
|
|
@ -2531,11 +2510,6 @@ func TestHostEncryptionKey(t *testing.T) {
|
|||
ds.GetHostArchivedDiskEncryptionKeyFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostArchivedDiskEncryptionKey, error) {
|
||||
return &fleet.HostArchivedDiskEncryptionKey{}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName,
|
||||
_ sqlx.QueryerContext,
|
||||
) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
|
||||
|
|
@ -2576,11 +2550,6 @@ func TestHostEncryptionKey(t *testing.T) {
|
|||
ds.GetHostArchivedDiskEncryptionKeyFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostArchivedDiskEncryptionKey, error) {
|
||||
return &fleet.HostArchivedDiskEncryptionKey{}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { // needed for new activity
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
|
|
@ -3057,11 +3026,6 @@ func TestLockUnlockWipeHostAuth(t *testing.T) {
|
|||
ds.GetHostMDMFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDM, error) {
|
||||
return &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.UnlockHostManuallyFunc = func(ctx context.Context, hostID uint, platform string, ts time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -3751,9 +3715,6 @@ func TestSetHostDeviceMapping(t *testing.T) {
|
|||
ds.ListHostDeviceMappingFunc = func(ctx context.Context, hostID uint) ([]*fleet.HostDeviceMapping, error) {
|
||||
return []*fleet.HostDeviceMapping{{HostID: hostID, Email: "user@example.com", Source: fleet.DeviceMappingMDMIdpAccounts}}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
|
|
@ -3789,9 +3750,6 @@ func TestSetHostDeviceMapping(t *testing.T) {
|
|||
ds.ListHostDeviceMappingFunc = func(ctx context.Context, hostID uint) ([]*fleet.HostDeviceMapping, error) {
|
||||
return []*fleet.HostDeviceMapping{{HostID: hostID, Email: "any@username.com", Source: fleet.DeviceMappingMDMIdpAccounts}}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
|
|
@ -3901,7 +3859,8 @@ func TestSetHostDeviceMapping(t *testing.T) {
|
|||
func TestDeleteHostDeviceIDPMapping(t *testing.T) {
|
||||
t.Run("success by admin on premium", func(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}})
|
||||
opts := &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}}
|
||||
svc, ctx := newTestService(t, ds, nil, nil, opts)
|
||||
|
||||
ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
|
||||
return &fleet.Host{ID: 1}, nil
|
||||
|
|
@ -3912,19 +3871,17 @@ func TestDeleteHostDeviceIDPMapping(t *testing.T) {
|
|||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
userCtx := test.UserContext(ctx, test.UserAdmin)
|
||||
err := svc.DeleteHostIDP(userCtx, 1)
|
||||
require.True(t, ds.DeleteHostIDPFuncInvoked)
|
||||
require.True(t, ds.NewActivityFuncInvoked)
|
||||
require.True(t, opts.ActivityMock.NewActivityFuncInvoked)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
t.Run("failure by admin on free", func(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierFree}})
|
||||
opts := &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierFree}}
|
||||
svc, ctx := newTestService(t, ds, nil, nil, opts)
|
||||
|
||||
ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
|
||||
return &fleet.Host{ID: 1}, nil
|
||||
|
|
@ -3935,9 +3892,6 @@ func TestDeleteHostDeviceIDPMapping(t *testing.T) {
|
|||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
userCtx := test.UserContext(ctx, test.UserAdmin)
|
||||
err := svc.DeleteHostIDP(userCtx, 1)
|
||||
|
|
@ -3945,7 +3899,7 @@ func TestDeleteHostDeviceIDPMapping(t *testing.T) {
|
|||
assert.Equal(t, fleet.ErrMissingLicense, err)
|
||||
|
||||
require.False(t, ds.DeleteHostIDPFuncInvoked)
|
||||
require.False(t, ds.NewActivityFuncInvoked)
|
||||
require.False(t, opts.ActivityMock.NewActivityFuncInvoked)
|
||||
})
|
||||
|
||||
t.Run("authorization tests", func(t *testing.T) {
|
||||
|
|
@ -3953,7 +3907,8 @@ func TestDeleteHostDeviceIDPMapping(t *testing.T) {
|
|||
globalHost := &fleet.Host{ID: 2}
|
||||
|
||||
ds := new(mock.Store)
|
||||
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}})
|
||||
opts := &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}}
|
||||
svc, ctx := newTestService(t, ds, nil, nil, opts)
|
||||
|
||||
ds.DeleteHostIDPFunc = func(ctx context.Context, id uint) error {
|
||||
return nil
|
||||
|
|
@ -3961,9 +3916,6 @@ func TestDeleteHostDeviceIDPMapping(t *testing.T) {
|
|||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
|
@ -4124,7 +4076,7 @@ func TestDeleteHostDeviceIDPMapping(t *testing.T) {
|
|||
for _, tc := range testCases {
|
||||
// reset ds mock flags
|
||||
ds.DeleteHostIDPFuncInvoked = false
|
||||
ds.NewActivityFuncInvoked = false
|
||||
opts.ActivityMock.NewActivityFuncInvoked = false
|
||||
|
||||
// redefine this datastore mock for each test case since its return value is specific per case
|
||||
ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
|
||||
|
|
@ -4143,11 +4095,11 @@ func TestDeleteHostDeviceIDPMapping(t *testing.T) {
|
|||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), authz.ForbiddenErrorMessage)
|
||||
require.False(t, ds.DeleteHostIDPFuncInvoked)
|
||||
require.False(t, ds.NewActivityFuncInvoked)
|
||||
require.False(t, opts.ActivityMock.NewActivityFuncInvoked)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.True(t, ds.DeleteHostIDPFuncInvoked)
|
||||
require.True(t, ds.NewActivityFuncInvoked)
|
||||
require.True(t, opts.ActivityMock.NewActivityFuncInvoked)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -182,11 +182,6 @@ func setupAuthTest(t *testing.T) (fleet.Datastore, map[string]fleet.User, *httpt
|
|||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
return ds, usersMap, server
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import (
|
|||
"github.com/WatchBeam/clock"
|
||||
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
|
||||
"github.com/fleetdm/fleet/v4/server"
|
||||
activity_api "github.com/fleetdm/fleet/v4/server/activity/api"
|
||||
"github.com/fleetdm/fleet/v4/server/config"
|
||||
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
|
|
@ -3376,15 +3377,15 @@ func (s *integrationTestSuite) TestListActivities() {
|
|||
|
||||
prevActivities := s.listActivities()
|
||||
|
||||
timestamp := time.Now()
|
||||
ctx = context.WithValue(ctx, fleet.ActivityWebhookContextKey, true)
|
||||
err := s.ds.NewActivity(ctx, &u, fleet.ActivityTypeAppliedSpecPack{}, nil, timestamp)
|
||||
activitySvc := mysql.NewTestActivityService(t, s.ds)
|
||||
apiUser := &activity_api.User{ID: u.ID, Name: u.Name, Email: u.Email}
|
||||
err := activitySvc.NewActivity(ctx, apiUser, fleet.ActivityTypeAppliedSpecPack{})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.ds.NewActivity(ctx, &u, fleet.ActivityTypeDeletedPack{}, nil, timestamp)
|
||||
err = activitySvc.NewActivity(ctx, apiUser, fleet.ActivityTypeDeletedPack{})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.ds.NewActivity(ctx, &u, fleet.ActivityTypeEditedPack{}, nil, timestamp)
|
||||
err = activitySvc.NewActivity(ctx, apiUser, fleet.ActivityTypeEditedPack{})
|
||||
require.NoError(t, err)
|
||||
|
||||
lenPage := len(prevActivities) + 2
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ func (s *liveQueriesTestSuite) SetupSuite() {
|
|||
lq := live_query_mock.New(s.T())
|
||||
s.lq = lq
|
||||
|
||||
opts := &TestServerOpts{Lq: lq, Rs: rs}
|
||||
opts := &TestServerOpts{Lq: lq, Rs: rs, DBConns: s.dbConns}
|
||||
if os.Getenv("FLEET_INTEGRATION_TESTS_DISABLE_LOG") != "" {
|
||||
opts.Logger = logging.NewNopLogger()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server/service/contract"
|
||||
"github.com/fleetdm/fleet/v4/server/service/integrationtest/scep_server"
|
||||
"github.com/fleetdm/fleet/v4/server/service/mock"
|
||||
"github.com/fleetdm/fleet/v4/server/service/modules/activities"
|
||||
activitiesmod "github.com/fleetdm/fleet/v4/server/service/modules/activities"
|
||||
"github.com/fleetdm/fleet/v4/server/service/osquery_utils"
|
||||
"github.com/fleetdm/fleet/v4/server/service/schedule"
|
||||
"github.com/fleetdm/fleet/v4/server/test"
|
||||
|
|
@ -221,7 +221,7 @@ func (s *integrationMDMTestSuite) SetupSuite() {
|
|||
wlog = logging.NewNopLogger()
|
||||
}
|
||||
|
||||
activityModule := activities.NewActivityModule(s.ds, wlog)
|
||||
activityModule := activitiesmod.NewActivityModule()
|
||||
androidMockClient := &android_mock.Client{}
|
||||
androidMockClient.SetAuthenticationSecretFunc = func(secret string) error {
|
||||
return nil
|
||||
|
|
@ -319,6 +319,7 @@ func (s *integrationMDMTestSuite) SetupSuite() {
|
|||
BootstrapPackageStore: bootstrapPackageStore,
|
||||
androidMockClient: androidMockClient,
|
||||
androidModule: androidSvc,
|
||||
ActivityModule: activityModule,
|
||||
StartCronSchedules: []TestNewScheduleFunc{
|
||||
func(ctx context.Context, ds fleet.Datastore) fleet.NewCronScheduleFunc {
|
||||
return func() (fleet.CronSchedule, error) {
|
||||
|
|
@ -452,9 +453,7 @@ func (s *integrationMDMTestSuite) SetupSuite() {
|
|||
ctx, name, s.T().Name(), 1*time.Hour, ds, ds,
|
||||
schedule.WithLogger(logger),
|
||||
schedule.WithJob("cron_iphone_ipad_refetcher", func(ctx context.Context) error {
|
||||
return apple_mdm.IOSiPadOSRefetch(ctx, ds, mdmCommander, logger.SlogLogger(), func(ctx context.Context, user *fleet.User, act fleet.ActivityDetails) error {
|
||||
return newActivity(ctx, user, act, ds, logger)
|
||||
})
|
||||
return apple_mdm.IOSiPadOSRefetch(ctx, ds, mdmCommander, logger.SlogLogger(), s.fleetSvc.NewActivity)
|
||||
}),
|
||||
)
|
||||
return refetcherSchedule, nil
|
||||
|
|
@ -484,6 +483,7 @@ func (s *integrationMDMTestSuite) SetupSuite() {
|
|||
|
||||
s.server = server
|
||||
s.users = users
|
||||
s.fleetSvc = svc
|
||||
s.token = s.getTestAdminToken()
|
||||
s.cachedAdminToken = s.token
|
||||
s.fleetCfg = fleetCfg
|
||||
|
|
@ -19744,9 +19744,7 @@ func (s *integrationMDMTestSuite) TestIOSiPadOSRefetch() {
|
|||
return nil, errors.New("unknown device")
|
||||
}
|
||||
|
||||
err = apple_mdm.IOSiPadOSRefetch(ctx, s.ds, s.mdmCommander, s.logger.SlogLogger(), func(ctx context.Context, user *fleet.User, act fleet.ActivityDetails) error {
|
||||
return newActivity(ctx, user, act, s.ds, s.logger)
|
||||
})
|
||||
err = apple_mdm.IOSiPadOSRefetch(ctx, s.ds, s.mdmCommander, s.logger.SlogLogger(), s.fleetSvc.NewActivity)
|
||||
require.NoError(s.T(), err) // Verify it not longer throws an error
|
||||
|
||||
// Verify successful is still enrolled
|
||||
|
|
@ -20760,9 +20758,7 @@ func (s *integrationMDMTestSuite) TestInstalledApplicationListCommandForBYODiDev
|
|||
checkExpectedCommands(mdmClientDEP, false, 1)
|
||||
|
||||
// run the cron-based refetch, will not do anything as the devices were just refetched
|
||||
err = apple_mdm.IOSiPadOSRefetch(ctx, s.ds, s.mdmCommander, s.logger.SlogLogger(), func(ctx context.Context, user *fleet.User, act fleet.ActivityDetails) error {
|
||||
return newActivity(ctx, user, act, s.ds, s.logger)
|
||||
})
|
||||
err = apple_mdm.IOSiPadOSRefetch(ctx, s.ds, s.mdmCommander, s.logger.SlogLogger(), s.fleetSvc.NewActivity)
|
||||
require.NoError(t, err)
|
||||
|
||||
checkExpectedCommands(mdmClientBYOD, true, 0)
|
||||
|
|
@ -20775,9 +20771,7 @@ func (s *integrationMDMTestSuite) TestInstalledApplicationListCommandForBYODiDev
|
|||
require.NoError(t, s.ds.UpdateHost(ctx, hostDEP))
|
||||
|
||||
// run the cron-based refetch again, will enqueue the commands with the correct managed only flag
|
||||
err = apple_mdm.IOSiPadOSRefetch(ctx, s.ds, s.mdmCommander, s.logger.SlogLogger(), func(ctx context.Context, user *fleet.User, act fleet.ActivityDetails) error {
|
||||
return newActivity(ctx, user, act, s.ds, s.logger)
|
||||
})
|
||||
err = apple_mdm.IOSiPadOSRefetch(ctx, s.ds, s.mdmCommander, s.logger.SlogLogger(), s.fleetSvc.NewActivity)
|
||||
require.NoError(t, err)
|
||||
|
||||
checkExpectedCommands(mdmClientBYOD, true, 1)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/config"
|
||||
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
android_mock "github.com/fleetdm/fleet/v4/server/mdm/android/mock"
|
||||
android_service "github.com/fleetdm/fleet/v4/server/mdm/android/service"
|
||||
|
|
@ -12,7 +13,7 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server/platform/logging"
|
||||
"github.com/fleetdm/fleet/v4/server/service"
|
||||
"github.com/fleetdm/fleet/v4/server/service/integrationtest"
|
||||
"github.com/fleetdm/fleet/v4/server/service/modules/activities"
|
||||
activitiesmod "github.com/fleetdm/fleet/v4/server/service/modules/activities"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
|
@ -26,7 +27,7 @@ func SetUpSuite(t *testing.T, uniqueTestName string) *Suite {
|
|||
logger := logging.NewLogfmtLogger(os.Stdout)
|
||||
proxy := android_mock.Client{}
|
||||
proxy.InitCommonMocks()
|
||||
activityModule := activities.NewActivityModule(ds, logger)
|
||||
activityModule := activitiesmod.NewActivityModule()
|
||||
androidSvc, err := android_service.NewServiceWithClient(
|
||||
logger.SlogLogger(),
|
||||
ds,
|
||||
|
|
@ -41,14 +42,17 @@ func SetUpSuite(t *testing.T, uniqueTestName string) *Suite {
|
|||
)
|
||||
require.NoError(t, err)
|
||||
androidSvc.(*android_service.Service).AllowLocalhostServerURL = true
|
||||
dbConns := mysql.TestDBConnections(t, ds)
|
||||
users, server := service.RunServerForTestsWithServiceWithDS(t, ctx, ds, fleetSvc, &service.TestServerOpts{
|
||||
License: &fleet.LicenseInfo{
|
||||
Tier: fleet.TierFree,
|
||||
},
|
||||
FleetConfig: &fleetCfg,
|
||||
Pool: redisPool,
|
||||
Logger: logger,
|
||||
FeatureRoutes: []endpointer.HandlerRoutesFunc{android_service.GetRoutes(fleetSvc, androidSvc)},
|
||||
FleetConfig: &fleetCfg,
|
||||
Pool: redisPool,
|
||||
Logger: logger,
|
||||
FeatureRoutes: []endpointer.HandlerRoutesFunc{android_service.GetRoutes(fleetSvc, androidSvc)},
|
||||
DBConns: dbConns,
|
||||
ActivityModule: activityModule,
|
||||
})
|
||||
|
||||
s := &Suite{
|
||||
|
|
|
|||
|
|
@ -3617,7 +3617,7 @@ func (svc *Service) UnenrollMDM(ctx context.Context, hostID uint) error {
|
|||
}
|
||||
installedFromDEP = info.InstalledFromDEP
|
||||
|
||||
mdmLifecycle := mdmlifecycle.New(svc.ds, svc.logger, newActivity)
|
||||
mdmLifecycle := mdmlifecycle.New(svc.ds, svc.logger, svc.NewActivity)
|
||||
err = mdmLifecycle.Do(ctx, mdmlifecycle.HostOptions{
|
||||
Action: mdmlifecycle.HostActionTurnOff,
|
||||
Platform: host.Platform,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/optjson"
|
||||
activity_api "github.com/fleetdm/fleet/v4/server/activity/api"
|
||||
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
|
||||
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
|
||||
|
|
@ -134,10 +135,6 @@ func TestMDMAppleAuthorization(t *testing.T) {
|
|||
return nil
|
||||
}
|
||||
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
|
@ -1213,9 +1210,6 @@ func TestMDMWindowsConfigProfileAuthz(t *testing.T) {
|
|||
},
|
||||
}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(context.Context, *fleet.User, fleet.ActivityDetails, []byte, time.Time) error {
|
||||
return nil
|
||||
}
|
||||
ds.GetMDMWindowsConfigProfileFunc = func(ctx context.Context, pid string) (*fleet.MDMWindowsConfigProfile, error) {
|
||||
var tid uint
|
||||
if pid == "team-1" {
|
||||
|
|
@ -1313,9 +1307,6 @@ func TestUploadWindowsMDMConfigProfileValidations(t *testing.T) {
|
|||
}
|
||||
return &fleet.Team{ID: tid, Name: "team1"}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(context.Context, *fleet.User, fleet.ActivityDetails, []byte, time.Time) error {
|
||||
return nil
|
||||
}
|
||||
ds.NewMDMWindowsConfigProfileFunc = func(ctx context.Context, cp fleet.MDMWindowsConfigProfile, usesFleetVars []fleet.FleetVarName) (*fleet.MDMWindowsConfigProfile, error) {
|
||||
if bytes.Contains(cp.SyncML, []byte("duplicate")) {
|
||||
return nil, &alreadyExistsError{}
|
||||
|
|
@ -1431,11 +1422,6 @@ func TestMDMBatchSetProfiles(t *testing.T) {
|
|||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string,
|
||||
hostUUIDs []string,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
|
|
@ -2231,9 +2217,6 @@ func TestMDMResendConfigProfileAuthz(t *testing.T) {
|
|||
ds.ResendHostMDMProfileFunc = func(ctx context.Context, hostUUID, profUUID string) error {
|
||||
return nil
|
||||
}
|
||||
ds.NewActivityFunc = func(context.Context, *fleet.User, fleet.ActivityDetails, []byte, time.Time) error {
|
||||
return nil
|
||||
}
|
||||
ds.BatchResendMDMProfileToHostsFunc = func(ctx context.Context, profUUID string, filters fleet.BatchResendMDMProfileFilters) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
|
@ -2551,7 +2534,8 @@ func TestUploadMDMAppleAPNSCertReplacesFileVaultProfile(t *testing.T) {
|
|||
// We want to verify here that the disk encryption profile get's deleted for apple.
|
||||
ds := new(mock.Store)
|
||||
lic := &fleet.LicenseInfo{Tier: fleet.TierPremium}
|
||||
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{SkipCreateTestUsers: true, License: lic})
|
||||
opts := &TestServerOpts{SkipCreateTestUsers: true, License: lic}
|
||||
svc, ctx := newTestService(t, ds, nil, nil, opts)
|
||||
ctx = test.UserContext(ctx, test.UserAdmin)
|
||||
ctx = license.NewContext(ctx, lic)
|
||||
|
||||
|
|
@ -2598,7 +2582,7 @@ func TestUploadMDMAppleAPNSCertReplacesFileVaultProfile(t *testing.T) {
|
|||
}
|
||||
|
||||
newActivityCalls := 0
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
opts.ActivityMock.NewActivityFunc = func(_ context.Context, _ *activity_api.User, activity activity_api.ActivityDetails) error {
|
||||
act := fleet.ActivityTypeEnabledMacosDiskEncryption{}
|
||||
require.Equal(t, act.ActivityName(), activity.ActivityName())
|
||||
newActivityCalls++
|
||||
|
|
@ -2686,11 +2670,6 @@ func TestNewMDMProfilePremiumOnlyAndroid(t *testing.T) {
|
|||
ds.TeamWithExtrasFunc = func(ctx context.Context, id uint) (*fleet.Team, error) {
|
||||
return &fleet.Team{ID: id, Name: "team"}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.ValidateEmbeddedSecretsFunc = func(ctx context.Context, documents []string) error {
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2123,7 +2123,7 @@ func (svc *Service) storeWindowsMDMEnrolledDevice(ctx context.Context, userID st
|
|||
displayName := reqDeviceName
|
||||
var serial string
|
||||
if hostUUID != "" {
|
||||
mdmLifecycle := mdmlifecycle.New(svc.ds, svc.logger, newActivity)
|
||||
mdmLifecycle := mdmlifecycle.New(svc.ds, svc.logger, svc.NewActivity)
|
||||
err = mdmLifecycle.Do(ctx, mdmlifecycle.HostOptions{
|
||||
Action: mdmlifecycle.HostActionTurnOn,
|
||||
Platform: "windows",
|
||||
|
|
|
|||
|
|
@ -2,118 +2,44 @@ package activities
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff"
|
||||
"github.com/fleetdm/fleet/v4/server"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
activity_api "github.com/fleetdm/fleet/v4/server/activity/api"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/platform/endpointer"
|
||||
"github.com/fleetdm/fleet/v4/server/platform/logging"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/go-kit/log/level"
|
||||
)
|
||||
|
||||
type activityModule struct {
|
||||
repo ActivityStore
|
||||
logger *logging.Logger
|
||||
}
|
||||
|
||||
// ActivityModule is a thin facade that translates fleet types to bounded context
|
||||
// types and delegates to the activity bounded context service.
|
||||
type ActivityModule interface {
|
||||
NewActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error
|
||||
}
|
||||
|
||||
// ActivityStore is the datastore interface needed to handle Fleet activities.
|
||||
// It is implemented by fleet.Datastore.
|
||||
type ActivityStore interface {
|
||||
AppConfig(ctx context.Context) (*fleet.AppConfig, error)
|
||||
NewActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error
|
||||
// Module is the concrete implementation of ActivityModule. It holds a reference
|
||||
// to the activity bounded context service, which must be set via SetService
|
||||
// before any calls to NewActivity.
|
||||
type Module struct {
|
||||
svc activity_api.NewActivityService
|
||||
}
|
||||
|
||||
func NewActivityModule(repo ActivityStore, logger *logging.Logger) ActivityModule {
|
||||
return &activityModule{
|
||||
repo: repo,
|
||||
logger: logger,
|
||||
}
|
||||
// NewActivityModule creates a new activity module. The bounded context service
|
||||
// must be set via SetService before use.
|
||||
func NewActivityModule() *Module {
|
||||
return &Module{}
|
||||
}
|
||||
|
||||
func (a *activityModule) NewActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
||||
appConfig, err := a.repo.AppConfig(ctx)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get app config")
|
||||
}
|
||||
// SetService sets the activity bounded context service.
|
||||
func (m *Module) SetService(svc activity_api.NewActivityService) {
|
||||
m.svc = svc
|
||||
}
|
||||
|
||||
detailsBytes, err := json.Marshal(activity)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "marshaling activity details")
|
||||
}
|
||||
// Duplicate JSON keys so that stored activity details include both the
|
||||
// old and new field names (e.g. team_id and fleet_id).
|
||||
if rules := endpointer.ExtractAliasRules(activity); len(rules) > 0 {
|
||||
detailsBytes = endpointer.DuplicateJSONKeys(detailsBytes, rules, endpointer.DuplicateJSONKeysOpts{Compact: true})
|
||||
}
|
||||
timestamp := time.Now()
|
||||
|
||||
if appConfig.WebhookSettings.ActivitiesWebhook.Enable {
|
||||
webhookURL := appConfig.WebhookSettings.ActivitiesWebhook.DestinationURL
|
||||
var userID *uint
|
||||
var userName *string
|
||||
var userEmail *string
|
||||
activityType := activity.ActivityName()
|
||||
|
||||
if user != nil {
|
||||
// To support creating activities with users that were deleted. This can happen
|
||||
// for automatically installed software which uses the author of the upload as the author of
|
||||
// the installation.
|
||||
if user.ID != 0 && !user.Deleted {
|
||||
userID = &user.ID
|
||||
}
|
||||
userName = &user.Name
|
||||
userEmail = &user.Email
|
||||
} else if automatableActivity, ok := activity.(fleet.AutomatableActivity); ok && automatableActivity.WasFromAutomation() {
|
||||
userName = ptr.String(fleet.ActivityAutomationAuthor)
|
||||
func (m *Module) NewActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
||||
var apiUser *activity_api.User
|
||||
if user != nil {
|
||||
apiUser = &activity_api.User{
|
||||
ID: user.ID,
|
||||
Name: user.Name,
|
||||
Email: user.Email,
|
||||
Deleted: user.Deleted,
|
||||
}
|
||||
|
||||
// TODO: webhook module? probably webhook job too tbh since this isn't very resilient
|
||||
go func() {
|
||||
retryStrategy := backoff.NewExponentialBackOff()
|
||||
retryStrategy.MaxElapsedTime = 30 * time.Minute
|
||||
err := backoff.Retry(
|
||||
func() error {
|
||||
if err := server.PostJSONWithTimeout(
|
||||
context.Background(), webhookURL, &fleet.ActivityWebhookPayload{
|
||||
Timestamp: timestamp,
|
||||
ActorFullName: userName,
|
||||
ActorID: userID,
|
||||
ActorEmail: userEmail,
|
||||
Type: activityType,
|
||||
Details: (*json.RawMessage)(&detailsBytes),
|
||||
},
|
||||
); err != nil {
|
||||
var statusCoder kithttp.StatusCoder
|
||||
if errors.As(err, &statusCoder) && statusCoder.StatusCode() == http.StatusTooManyRequests {
|
||||
level.Debug(a.logger).Log("msg", "fire activity webhook", "err", err)
|
||||
return err
|
||||
}
|
||||
return backoff.Permanent(err)
|
||||
}
|
||||
return nil
|
||||
}, retryStrategy,
|
||||
)
|
||||
if err != nil {
|
||||
level.Error(a.logger).Log(
|
||||
"msg", fmt.Sprintf("fire activity webhook to %s", server.MaskSecretURLParams(webhookURL)), "err",
|
||||
server.MaskURLError(err).Error(),
|
||||
)
|
||||
}
|
||||
}()
|
||||
}
|
||||
// We update the context to indicate that we processed the webhook.
|
||||
ctx = context.WithValue(ctx, fleet.ActivityWebhookContextKey, true)
|
||||
return a.repo.NewActivity(ctx, user, activity, detailsBytes, timestamp)
|
||||
return m.svc.NewActivity(ctx, apiUser, activity)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"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/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm"
|
||||
|
|
@ -175,7 +175,8 @@ func TestOrbitLUKSDataSave(t *testing.T) {
|
|||
t.Run("when private key is set", func(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
license := &fleet.LicenseInfo{Tier: fleet.TierPremium}
|
||||
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true})
|
||||
opts := &TestServerOpts{License: license, SkipCreateTestUsers: true}
|
||||
svc, ctx := newTestService(t, ds, nil, nil, opts)
|
||||
host := &fleet.Host{
|
||||
OsqueryHostID: ptr.String("test"),
|
||||
ID: 1,
|
||||
|
|
@ -190,15 +191,8 @@ func TestOrbitLUKSDataSave(t *testing.T) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context,
|
||||
user *fleet.User,
|
||||
activity fleet.ActivityDetails,
|
||||
details []byte,
|
||||
createdAt time.Time,
|
||||
) error {
|
||||
opts.ActivityMock.NewActivityFunc = func(_ context.Context, _ *activity_api.User, activity activity_api.ActivityDetails) error {
|
||||
require.Equal(t, activity.ActivityName(), fleet.ActivityTypeEscrowedDiskEncryptionKey{}.ActivityName())
|
||||
require.NotEmpty(t, details)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -262,6 +256,7 @@ func TestOrbitLUKSDataSave(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
require.False(t, ds.ReportEscrowErrorFuncInvoked)
|
||||
require.True(t, ds.SaveLUKSDataFuncInvoked)
|
||||
require.True(t, opts.ActivityMock.NewActivityFuncInvoked)
|
||||
})
|
||||
|
||||
t.Run("fail when no/invalid private key is set", func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -341,10 +341,6 @@ func TestEnrollOsqueryEnforceLimit(t *testing.T) {
|
|||
ds.GetHostIdentityCertByNameFunc = func(ctx context.Context, name string) (*types.HostIdentityCertificate, error) {
|
||||
return nil, newNotFoundError()
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
redisWrapDS := mysqlredis.New(ds, pool, mysqlredis.WithEnforcedHostLimit(maxHosts))
|
||||
svc, ctx := newTestService(t, redisWrapDS, nil, nil, &TestServerOpts{
|
||||
EnrollHostLimiter: redisWrapDS,
|
||||
|
|
@ -3220,11 +3216,6 @@ func TestObserversCanOnlyRunDistributedCampaigns(t *testing.T) {
|
|||
})
|
||||
|
||||
q := "select year, month, day, hour, minutes, seconds from time"
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
_, err := svc.NewDistributedQueryCampaign(viewerCtx, q, nil, fleet.HostTargets{HostIDs: []uint{2}, LabelIDs: []uint{1}})
|
||||
require.Error(t, err)
|
||||
|
||||
|
|
@ -3260,11 +3251,6 @@ func TestObserversCanOnlyRunDistributedCampaigns(t *testing.T) {
|
|||
ds.HostIDsInTargetsFunc = func(ctx context.Context, filter fleet.TeamFilter, targets fleet.HostTargets) ([]uint, error) {
|
||||
return []uint{1, 3, 5}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
lq.On("RunQuery", "21", "select 1;", []uint{1, 3, 5}).Return(nil)
|
||||
_, err = svc.NewDistributedQueryCampaign(viewerCtx, "", ptr.Uint(42), fleet.HostTargets{HostIDs: []uint{2}, LabelIDs: []uint{1}})
|
||||
require.NoError(t, err)
|
||||
|
|
@ -3302,11 +3288,6 @@ func TestTeamMaintainerCanRunNewDistributedCampaigns(t *testing.T) {
|
|||
})
|
||||
|
||||
q := "select year, month, day, hour, minutes, seconds from time"
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
// var gotQuery *fleet.Query
|
||||
ds.NewQueryFunc = func(ctx context.Context, query *fleet.Query, opts ...fleet.OptionalArg) (*fleet.Query, error) {
|
||||
// gotQuery = query
|
||||
|
|
@ -3326,11 +3307,6 @@ func TestTeamMaintainerCanRunNewDistributedCampaigns(t *testing.T) {
|
|||
ds.HostIDsInTargetsFunc = func(ctx context.Context, filter fleet.TeamFilter, targets fleet.HostTargets) ([]uint, error) {
|
||||
return []uint{1, 3, 5}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
lq.On("RunQuery", "0", "select year, month, day, hour, minutes, seconds from time", []uint{1, 3, 5}).Return(nil)
|
||||
_, err := svc.NewDistributedQueryCampaign(viewerCtx, q, nil, fleet.HostTargets{HostIDs: []uint{2}, LabelIDs: []uint{1}, TeamIDs: []uint{123}})
|
||||
require.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package service
|
|||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/authz"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
||||
|
|
@ -38,7 +37,8 @@ func TestGetPack(t *testing.T) {
|
|||
|
||||
func TestNewPackSavesTargets(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
svc, ctx := newTestService(t, ds, nil, nil)
|
||||
opts := &TestServerOpts{}
|
||||
svc, ctx := newTestService(t, ds, nil, nil, opts)
|
||||
|
||||
ds.NewPackFunc = func(ctx context.Context, pack *fleet.Pack, opts ...fleet.OptionalArg) (*fleet.Pack, error) {
|
||||
return pack, nil
|
||||
|
|
@ -46,12 +46,6 @@ func TestNewPackSavesTargets(t *testing.T) {
|
|||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
packPayload := fleet.PackPayload{
|
||||
Name: ptr.String("foo"),
|
||||
HostIDs: &[]uint{123},
|
||||
|
|
@ -68,7 +62,7 @@ func TestNewPackSavesTargets(t *testing.T) {
|
|||
assert.Equal(t, uint(456), pack.LabelIDs[0])
|
||||
assert.Equal(t, uint(789), pack.TeamIDs[0])
|
||||
assert.True(t, ds.NewPackFuncInvoked)
|
||||
assert.True(t, ds.NewActivityFuncInvoked)
|
||||
assert.True(t, opts.ActivityMock.NewActivityFuncInvoked)
|
||||
}
|
||||
|
||||
func TestPacksWithDS(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
activity_api "github.com/fleetdm/fleet/v4/server/activity/api"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
||||
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
|
|
@ -23,15 +24,14 @@ func TestQueryPayloadValidationCreate(t *testing.T) {
|
|||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
opts := &TestServerOpts{}
|
||||
svc, ctx := newTestService(t, ds, nil, nil, opts)
|
||||
opts.ActivityMock.NewActivityFunc = func(_ context.Context, _ *activity_api.User, activity activity_api.ActivityDetails) error {
|
||||
act, ok := activity.(fleet.ActivityTypeCreatedSavedQuery)
|
||||
assert.True(t, ok)
|
||||
assert.NotEmpty(t, act.Name)
|
||||
return nil
|
||||
}
|
||||
svc, ctx := newTestService(t, ds, nil, nil)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
|
@ -153,17 +153,15 @@ func TestQueryPayloadValidationModify(t *testing.T) {
|
|||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
opts := &TestServerOpts{}
|
||||
svc, ctx := newTestService(t, ds, nil, nil, opts)
|
||||
opts.ActivityMock.NewActivityFunc = func(_ context.Context, _ *activity_api.User, activity activity_api.ActivityDetails) error {
|
||||
act, ok := activity.(fleet.ActivityTypeEditedSavedQuery)
|
||||
assert.True(t, ok)
|
||||
assert.NotEmpty(t, act.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
svc, ctx := newTestService(t, ds, nil, nil)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
queryPayload fleet.QueryPayload
|
||||
|
|
@ -372,11 +370,6 @@ func TestQueryAuth(t *testing.T) {
|
|||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.QueryFunc = func(ctx context.Context, id uint) (*fleet.Query, error) {
|
||||
if id == 99 { //nolint:gocritic // ignore ifElseChain
|
||||
return &globalQuery, nil
|
||||
|
|
@ -967,11 +960,6 @@ func TestApplyQuerySpec(t *testing.T) {
|
|||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string) (*fleet.Query, error) {
|
||||
return nil, newNotFoundError()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -539,11 +539,6 @@ func TestSavedScripts(t *testing.T) {
|
|||
ds.ListScriptsFunc = func(ctx context.Context, teamID *uint, opt fleet.ListOptions) ([]*fleet.Script, *fleet.PaginationMetadata, error) {
|
||||
return nil, &fleet.PaginationMetadata{}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.TeamWithExtrasFunc = func(ctx context.Context, id uint) (*fleet.Team, error) {
|
||||
return &fleet.Team{ID: 0}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/WatchBeam/clock"
|
||||
activity_api "github.com/fleetdm/fleet/v4/server/activity/api"
|
||||
"github.com/fleetdm/fleet/v4/server/authz"
|
||||
"github.com/fleetdm/fleet/v4/server/config"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
|
|
@ -71,6 +72,10 @@ type Service struct {
|
|||
keyValueStore fleet.KeyValueStore
|
||||
|
||||
androidSvc android.Service
|
||||
|
||||
// activitySvc is the activity bounded context service for creating activities.
|
||||
// When set, NewActivity delegates to this service instead of the legacy implementation.
|
||||
activitySvc activity_api.NewActivityService
|
||||
}
|
||||
|
||||
// ConditionalAccessMicrosoftProxy is the interface of the Microsoft compliance proxy.
|
||||
|
|
@ -192,6 +197,12 @@ func (svc *Service) SendEmail(ctx context.Context, mail fleet.Email) error {
|
|||
return svc.mailService.SendEmail(ctx, mail)
|
||||
}
|
||||
|
||||
// SetActivityService sets the activity bounded context service for creating activities.
|
||||
// This should be called after NewService to inject the activity service dependency.
|
||||
func (svc *Service) SetActivityService(activitySvc activity_api.NewActivityService) {
|
||||
svc.activitySvc = activitySvc
|
||||
}
|
||||
|
||||
type validationMiddleware struct {
|
||||
fleet.Service
|
||||
ds fleet.Datastore
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import (
|
|||
"fmt"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/config"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
|
|
@ -102,9 +101,6 @@ func TestEmptyEnrollSecret(t *testing.T) {
|
|||
ds.GetEnrollSecretsFunc = func(ctx context.Context, teamID *uint) ([]*fleet.EnrollSecret, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := svc.ApplyEnrollSecretSpec(
|
||||
test.UserContext(ctx, test.UserAdmin),
|
||||
|
|
|
|||
|
|
@ -61,11 +61,6 @@ func TestStreamCampaignResultsClosesReditOnWSClose(t *testing.T) {
|
|||
ds.CountHostsInTargetsFunc = func(ctx context.Context, filter fleet.TeamFilter, targets fleet.HostTargets, now time.Time) (fleet.TargetMetrics, error) {
|
||||
return fleet.TargetMetrics{TotalHosts: 1}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.SessionByKeyFunc = func(ctx context.Context, key string) (*fleet.Session, error) {
|
||||
return &fleet.Session{
|
||||
CreateTimestamp: fleet.CreateTimestamp{CreatedAt: time.Now()},
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
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/datastore/mysql"
|
||||
|
|
@ -154,7 +155,8 @@ func TestAuthenticate(t *testing.T) {
|
|||
|
||||
func TestMFA(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
svc, ctx := newTestService(t, ds, nil, nil)
|
||||
opts := &TestServerOpts{}
|
||||
svc, ctx := newTestService(t, ds, nil, nil, opts)
|
||||
|
||||
user := &fleet.User{MFAEnabled: true, Name: "Bob Smith", Email: "foo@example.com"}
|
||||
require.NoError(t, user.SetPassword(test.GoodPassword, 10, 10))
|
||||
|
|
@ -200,15 +202,15 @@ func TestMFA(t *testing.T) {
|
|||
|
||||
session = &fleet.Session{}
|
||||
mfaUser = user
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
require.Equal(t, mfaUser, user)
|
||||
opts.ActivityMock.NewActivityFunc = func(_ context.Context, user *activity_api.User, activity activity_api.ActivityDetails) error {
|
||||
require.Equal(t, mfaUser.Email, user.Email)
|
||||
require.Equal(t, fleet.ActivityTypeUserLoggedIn{}.ActivityName(), activity.ActivityName())
|
||||
return nil
|
||||
}
|
||||
resp, err = sessionCreateEndpoint(ctx, &sessionCreateRequest{Token: mfaToken}, svc)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, resp.Error())
|
||||
require.True(t, ds.NewActivityFuncInvoked)
|
||||
require.True(t, opts.ActivityMock.NewActivityFuncInvoked)
|
||||
}
|
||||
|
||||
func TestGetSessionByKey(t *testing.T) {
|
||||
|
|
@ -300,12 +302,6 @@ func TestGetSSOUser(t *testing.T) {
|
|||
},
|
||||
})
|
||||
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{
|
||||
SSOSettings: &fleet.SSOSettings{
|
||||
|
|
|
|||
|
|
@ -111,12 +111,6 @@ func TestSoftwareInstallersAuth(t *testing.T) {
|
|||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
ds.TeamLiteFunc = func(ctx context.Context, tid uint) (*fleet.TeamLite, error) {
|
||||
if tt.teamID != nil {
|
||||
return &fleet.TeamLite{ID: *tt.teamID}, nil
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/file"
|
||||
activity_api "github.com/fleetdm/fleet/v4/server/activity/api"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
||||
"github.com/fleetdm/fleet/v4/server/datastore/s3"
|
||||
|
|
@ -116,10 +117,11 @@ func TestUploadSoftwareTitleIcon(t *testing.T) {
|
|||
ds := new(mock.Store)
|
||||
|
||||
mockIconStore := s3.SetupTestSoftwareTitleIconStore(t, "software-title-icons-unit-test", "icon-store-prefix")
|
||||
svc, _ := newTestService(t, ds, nil, nil, &TestServerOpts{
|
||||
opts := &TestServerOpts{
|
||||
License: &fleet.LicenseInfo{Tier: fleet.TierPremium},
|
||||
SoftwareTitleIconStore: mockIconStore,
|
||||
})
|
||||
}
|
||||
svc, _ := newTestService(t, ds, nil, nil, opts)
|
||||
defer func() {
|
||||
_, err := mockIconStore.Cleanup(ctx, []string{}, time.Now().Add(time.Hour))
|
||||
require.NoError(t, err)
|
||||
|
|
@ -128,8 +130,8 @@ func TestUploadSoftwareTitleIcon(t *testing.T) {
|
|||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
var capturedActivities []fleet.ActivityDetails
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, detailsBytes []byte, timestamp time.Time) error {
|
||||
var capturedActivities []activity_api.ActivityDetails
|
||||
opts.ActivityMock.NewActivityFunc = func(_ context.Context, _ *activity_api.User, activity activity_api.ActivityDetails) error {
|
||||
capturedActivities = append(capturedActivities, activity)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -143,7 +145,7 @@ func TestUploadSoftwareTitleIcon(t *testing.T) {
|
|||
{
|
||||
name: "upload icon title with no software installer, vpp app, or in-house app",
|
||||
before: func() {
|
||||
capturedActivities = make([]fleet.ActivityDetails, 0)
|
||||
capturedActivities = make([]activity_api.ActivityDetails, 0)
|
||||
ds.GetSoftwareInstallerMetadataByTeamAndTitleIDFunc = func(ctx context.Context, teamID *uint, titleID uint, includeUnpublished bool) (*fleet.SoftwareInstaller, error) {
|
||||
return nil, ctxerr.Wrap(ctx, &common_mysql.NotFoundError{ResourceType: "SoftwareInstaller"}, "get software installer")
|
||||
}
|
||||
|
|
@ -175,7 +177,7 @@ func TestUploadSoftwareTitleIcon(t *testing.T) {
|
|||
{
|
||||
name: "upload icon for software installer",
|
||||
before: func() {
|
||||
capturedActivities = make([]fleet.ActivityDetails, 0)
|
||||
capturedActivities = make([]activity_api.ActivityDetails, 0)
|
||||
ds.GetSoftwareInstallerMetadataByTeamAndTitleIDFunc = func(ctx context.Context, teamID *uint, titleID uint, includeUnpublished bool) (*fleet.SoftwareInstaller, error) {
|
||||
return &fleet.SoftwareInstaller{
|
||||
TitleID: ptr.Uint(1),
|
||||
|
|
@ -225,7 +227,7 @@ func TestUploadSoftwareTitleIcon(t *testing.T) {
|
|||
{
|
||||
name: "upload icon for vpp app",
|
||||
before: func() {
|
||||
capturedActivities = make([]fleet.ActivityDetails, 0)
|
||||
capturedActivities = make([]activity_api.ActivityDetails, 0)
|
||||
ds.GetSoftwareInstallerMetadataByTeamAndTitleIDFunc = func(ctx context.Context, teamID *uint, titleID uint, includeUnpublished bool) (*fleet.SoftwareInstaller, error) {
|
||||
return nil, ctxerr.Wrap(ctx, &common_mysql.NotFoundError{ResourceType: "SoftwareInstaller"}, "get software installer")
|
||||
}
|
||||
|
|
@ -279,7 +281,7 @@ func TestUploadSoftwareTitleIcon(t *testing.T) {
|
|||
{
|
||||
name: "upload icon for in-house app",
|
||||
before: func() {
|
||||
capturedActivities = make([]fleet.ActivityDetails, 0)
|
||||
capturedActivities = make([]activity_api.ActivityDetails, 0)
|
||||
ds.GetInHouseAppMetadataByTeamAndTitleIDFunc = func(ctx context.Context, teamID *uint, titleID uint) (*fleet.SoftwareInstaller, error) {
|
||||
return &fleet.SoftwareInstaller{
|
||||
TitleID: ptr.Uint(1),
|
||||
|
|
@ -340,14 +342,15 @@ func TestDeleteSoftwareTitleIcon(t *testing.T) {
|
|||
user := &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}
|
||||
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: user})
|
||||
ds := new(mock.Store)
|
||||
svc, _ := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}})
|
||||
deleteOpts := &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}}
|
||||
svc, _ := newTestService(t, ds, nil, nil, deleteOpts)
|
||||
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
|
||||
var capturedActivities []fleet.ActivityDetails
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, detailsBytes []byte, timestamp time.Time) error {
|
||||
var capturedActivities []activity_api.ActivityDetails
|
||||
deleteOpts.ActivityMock.NewActivityFunc = func(_ context.Context, _ *activity_api.User, activity activity_api.ActivityDetails) error {
|
||||
capturedActivities = append(capturedActivities, activity)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -360,7 +363,7 @@ func TestDeleteSoftwareTitleIcon(t *testing.T) {
|
|||
{
|
||||
"Delete existing icon for software installer",
|
||||
func() {
|
||||
capturedActivities = make([]fleet.ActivityDetails, 0)
|
||||
capturedActivities = make([]activity_api.ActivityDetails, 0)
|
||||
ds.ActivityDetailsForSoftwareTitleIconFunc = func(ctx context.Context, teamID uint, titleID uint) (fleet.DetailsForSoftwareIconActivity, error) {
|
||||
return fleet.DetailsForSoftwareIconActivity{
|
||||
SoftwareInstallerID: ptr.Uint(1),
|
||||
|
|
@ -406,7 +409,7 @@ func TestDeleteSoftwareTitleIcon(t *testing.T) {
|
|||
{
|
||||
"Delete existing icon for vpp app",
|
||||
func() {
|
||||
capturedActivities = make([]fleet.ActivityDetails, 0)
|
||||
capturedActivities = make([]activity_api.ActivityDetails, 0)
|
||||
ds.ActivityDetailsForSoftwareTitleIconFunc = func(ctx context.Context, teamID uint, titleID uint) (fleet.DetailsForSoftwareIconActivity, error) {
|
||||
platform := fleet.MacOSPlatform
|
||||
return fleet.DetailsForSoftwareIconActivity{
|
||||
|
|
@ -454,7 +457,7 @@ func TestDeleteSoftwareTitleIcon(t *testing.T) {
|
|||
{
|
||||
"Delete an already deleted icon",
|
||||
func() {
|
||||
capturedActivities = make([]fleet.ActivityDetails, 0)
|
||||
capturedActivities = make([]activity_api.ActivityDetails, 0)
|
||||
ds.ActivityDetailsForSoftwareTitleIconFunc = func(ctx context.Context, teamID uint, titleID uint) (fleet.DetailsForSoftwareIconActivity, error) {
|
||||
return fleet.DetailsForSoftwareIconActivity{}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package service
|
|||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/authz"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
||||
|
|
@ -60,11 +59,6 @@ func TestTeamPoliciesAuth(t *testing.T) {
|
|||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.TeamLiteFunc = func(ctx context.Context, tid uint) (*fleet.TeamLite, error) {
|
||||
return &fleet.TeamLite{ID: 1}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package service
|
|||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
|
|
@ -40,11 +39,6 @@ func TestTeamScheduleAuth(t *testing.T) {
|
|||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.NewQueryFunc = func(ctx context.Context, query *fleet.Query, opts ...fleet.OptionalArg) (*fleet.Query, error) {
|
||||
return &fleet.Query{}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
activity_api "github.com/fleetdm/fleet/v4/server/activity/api"
|
||||
authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
|
|
@ -27,11 +28,6 @@ func TestTeamAuth(t *testing.T) {
|
|||
ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
||||
return &fleet.Team{}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.TeamLiteFunc = func(ctx context.Context, tid uint) (*fleet.TeamLite, error) {
|
||||
return &fleet.TeamLite{}, nil
|
||||
}
|
||||
|
|
@ -218,7 +214,8 @@ func TestTeamAuth(t *testing.T) {
|
|||
func TestApplyTeamSpecs(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
|
||||
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true})
|
||||
opts := &TestServerOpts{License: license, SkipCreateTestUsers: true}
|
||||
svc, ctx := newTestService(t, ds, nil, nil, opts)
|
||||
user := &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}
|
||||
ctx = viewer.NewContext(ctx, viewer.Viewer{User: user})
|
||||
baseFeatures := fleet.Features{
|
||||
|
|
@ -297,9 +294,7 @@ func TestApplyTeamSpecs(t *testing.T) {
|
|||
return team, nil
|
||||
}
|
||||
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
opts.ActivityMock.NewActivityFunc = func(_ context.Context, _ *activity_api.User, activity activity_api.ActivityDetails) error {
|
||||
act := activity.(fleet.ActivityTypeAppliedSpecTeam)
|
||||
require.Len(t, act.Teams, 1)
|
||||
return nil
|
||||
|
|
@ -383,9 +378,7 @@ func TestApplyTeamSpecs(t *testing.T) {
|
|||
return &fleet.Team{ID: 123}, nil
|
||||
}
|
||||
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
opts.ActivityMock.NewActivityFunc = func(_ context.Context, _ *activity_api.User, activity activity_api.ActivityDetails) error {
|
||||
act := activity.(fleet.ActivityTypeAppliedSpecTeam)
|
||||
require.Len(t, act.Teams, 1)
|
||||
return nil
|
||||
|
|
@ -418,12 +411,6 @@ func TestApplyTeamSpecEnrollSecretForNewTeams(t *testing.T) {
|
|||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
t.Run("creates enroll secret when not included for a new team spec", func(t *testing.T) {
|
||||
ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
||||
require.Len(t, team.Secrets, 1)
|
||||
|
|
|
|||
|
|
@ -81,6 +81,8 @@ type withServer struct {
|
|||
lq *live_query_mock.MockLiveQuery
|
||||
|
||||
redisPool fleet.RedisPool
|
||||
|
||||
fleetSvc fleet.Service
|
||||
}
|
||||
|
||||
func (ts *withServer) SetupSuite(dbName string) {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/ee/server/service/est"
|
||||
"github.com/fleetdm/fleet/v4/ee/server/service/hostidentity"
|
||||
"github.com/fleetdm/fleet/v4/ee/server/service/hostidentity/httpsig"
|
||||
"github.com/fleetdm/fleet/v4/server"
|
||||
"github.com/fleetdm/fleet/v4/server/acl/activityacl"
|
||||
activity_api "github.com/fleetdm/fleet/v4/server/activity/api"
|
||||
activity_bootstrap "github.com/fleetdm/fleet/v4/server/activity/bootstrap"
|
||||
"github.com/fleetdm/fleet/v4/server/authz"
|
||||
"github.com/fleetdm/fleet/v4/server/config"
|
||||
|
|
@ -47,6 +49,7 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push"
|
||||
nanomdm_push "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push"
|
||||
scep_depot "github.com/fleetdm/fleet/v4/server/mdm/scep/depot"
|
||||
fleet_mock "github.com/fleetdm/fleet/v4/server/mock"
|
||||
nanodep_mock "github.com/fleetdm/fleet/v4/server/mock/nanodep"
|
||||
"github.com/fleetdm/fleet/v4/server/platform/endpointer"
|
||||
platformlogging "github.com/fleetdm/fleet/v4/server/platform/logging"
|
||||
|
|
@ -55,6 +58,7 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server/service/async"
|
||||
"github.com/fleetdm/fleet/v4/server/service/middleware/auth"
|
||||
"github.com/fleetdm/fleet/v4/server/service/mock"
|
||||
activitiesmod "github.com/fleetdm/fleet/v4/server/service/modules/activities"
|
||||
"github.com/fleetdm/fleet/v4/server/service/redis_key_value"
|
||||
"github.com/fleetdm/fleet/v4/server/service/redis_lock"
|
||||
"github.com/fleetdm/fleet/v4/server/sso"
|
||||
|
|
@ -282,6 +286,19 @@ func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig conf
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
// Set up mock activity service for unit tests. When DBConns is provided,
|
||||
// RunServerForTestsWithServiceWithDS will overwrite this with the real bounded context.
|
||||
activityMock := &fleet_mock.MockNewActivityService{
|
||||
NewActivityFunc: func(_ context.Context, _ *activity_api.User, _ activity_api.ActivityDetails) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
svc.SetActivityService(activityMock)
|
||||
if len(opts) > 0 {
|
||||
opts[0].ActivityMock = activityMock
|
||||
}
|
||||
|
||||
return svc, ctx
|
||||
}
|
||||
|
||||
|
|
@ -420,8 +437,13 @@ type TestServerOpts struct {
|
|||
HostIdentity *HostIdentity
|
||||
androidMockClient *android_mock.Client
|
||||
androidModule android.Service
|
||||
ActivityModule *activitiesmod.Module
|
||||
ConditionalAccess *ConditionalAccess
|
||||
DBConns *common_mysql.DBConnections
|
||||
|
||||
// ActivityMock is populated automatically by newTestServiceWithConfig.
|
||||
// After setup, tests can use it to intercept or assert on activity creation.
|
||||
ActivityMock *fleet_mock.MockNewActivityService
|
||||
}
|
||||
|
||||
func RunServerForTestsWithDS(t *testing.T, ds fleet.Datastore, opts ...*TestServerOpts) (map[string]fleet.User, *httptest.Server) {
|
||||
|
|
@ -464,12 +486,17 @@ func RunServerForTestsWithServiceWithDS(t *testing.T, ctx context.Context, ds fl
|
|||
require.NoError(t, err)
|
||||
activityAuthorizer := authz.NewAuthorizerAdapter(legacyAuthorizer)
|
||||
activityACLAdapter := activityacl.NewFleetServiceAdapter(svc)
|
||||
_, activityRoutesFn := activity_bootstrap.New(
|
||||
activitySvc, activityRoutesFn := activity_bootstrap.New(
|
||||
opts[0].DBConns,
|
||||
activityAuthorizer,
|
||||
activityACLAdapter,
|
||||
server.PostJSONWithTimeout,
|
||||
logger.SlogLogger(),
|
||||
)
|
||||
svc.SetActivityService(activitySvc)
|
||||
if opts[0].ActivityModule != nil {
|
||||
opts[0].ActivityModule.SetService(activitySvc)
|
||||
}
|
||||
activityAuthMiddleware := func(next endpoint.Endpoint) endpoint.Endpoint {
|
||||
return auth.AuthenticatedUser(svc, next)
|
||||
}
|
||||
|
|
@ -501,8 +528,8 @@ func RunServerForTestsWithServiceWithDS(t *testing.T, ctx context.Context, ds fl
|
|||
commander := apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPusher)
|
||||
if mdmStorage != nil && scepStorage != nil {
|
||||
vppInstaller := svc.(fleet.AppleMDMVPPInstaller)
|
||||
checkInAndCommand := NewMDMAppleCheckinAndCommandService(ds, commander, vppInstaller, opts[0].License.IsPremium(), logger, redis_key_value.New(redisPool))
|
||||
checkInAndCommand.RegisterResultsHandler("InstalledApplicationList", NewInstalledApplicationListResultsHandler(ds, commander, logger, cfg.Server.VPPVerifyTimeout, cfg.Server.VPPVerifyRequestDelay))
|
||||
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))
|
||||
err := RegisterAppleMDMProtocolServices(
|
||||
rootMux,
|
||||
|
|
|
|||
|
|
@ -98,11 +98,6 @@ func TestUserAuth(t *testing.T) {
|
|||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.CountGlobalAdminsFunc = func(ctx context.Context) (int, error) {
|
||||
return 2, nil // Return 2 to allow operations that check for last admin
|
||||
}
|
||||
|
|
@ -589,9 +584,6 @@ func TestMFAHandling(t *testing.T) {
|
|||
user.ID = 4
|
||||
return user, nil
|
||||
}
|
||||
ms.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
user, _, err := svc.CreateUser(ctx, payload)
|
||||
require.NoError(t, err)
|
||||
require.False(t, user.MFAEnabled)
|
||||
|
|
@ -1542,10 +1534,6 @@ func TestDeleteUserLastAdminProtection(t *testing.T) {
|
|||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := svc.DeleteUser(ctx, adminUser.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, ds.DeleteUserFuncInvoked)
|
||||
|
|
@ -1592,10 +1580,6 @@ func TestDeleteUserLastAdminProtection(t *testing.T) {
|
|||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := svc.DeleteUser(ctx, maintainerUser.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, ds.DeleteUserFuncInvoked)
|
||||
|
|
@ -1662,10 +1646,6 @@ func TestModifyUserLastAdminProtection(t *testing.T) {
|
|||
ds.SaveUserFunc = func(ctx context.Context, u *fleet.User) error {
|
||||
return nil
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := svc.ModifyUser(ctx, adminUser.ID, fleet.UserPayload{
|
||||
GlobalRole: ptr.String(fleet.RoleMaintainer),
|
||||
})
|
||||
|
|
@ -1679,10 +1659,6 @@ func TestModifyUserLastAdminProtection(t *testing.T) {
|
|||
ds.SaveUserFunc = func(ctx context.Context, u *fleet.User) error {
|
||||
return nil
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := svc.ModifyUser(ctx, adminUser.ID, fleet.UserPayload{
|
||||
GlobalRole: ptr.String(fleet.RoleAdmin),
|
||||
})
|
||||
|
|
@ -1732,10 +1708,6 @@ func TestPasswordChangeClearsTokensAndSessions(t *testing.T) {
|
|||
ds.SaveUserFunc = func(ctx context.Context, u *fleet.User) error {
|
||||
return nil
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var deletedPasswordResetForUserID uint
|
||||
ds.DeletePasswordResetRequestsForUserFunc = func(ctx context.Context, userID uint) error {
|
||||
deletedPasswordResetForUserID = userID
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
||||
|
|
@ -137,9 +136,8 @@ func CreateHostVPPAppInstallUpcomingActivity(t *testing.T, ds fleet.Datastore, h
|
|||
// The adamID is the one for the VPP app created by that call, and status is
|
||||
// one of the Apple MDM status string (Acknowledged, Error, CommandFormatError,
|
||||
// etc).
|
||||
func SetHostVPPAppInstallResult(t *testing.T, ds fleet.Datastore, nanods storage.CommandAndReportResultsStore, host *fleet.Host, execID, adamID, status string) {
|
||||
func SetHostVPPAppInstallResult(t *testing.T, ds fleet.Datastore, nanods storage.CommandAndReportResultsStore, host *fleet.Host, execID, adamID, status string, newActivityFn fleet.NewActivityFunc) {
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, fleet.ActivityWebhookContextKey, true)
|
||||
nanoCtx := &mdm.Request{EnrollID: &mdm.EnrollID{ID: host.UUID}, Context: ctx}
|
||||
|
||||
cmdRes := &mdm.CommandResults{
|
||||
|
|
@ -149,12 +147,12 @@ func SetHostVPPAppInstallResult(t *testing.T, ds fleet.Datastore, nanods storage
|
|||
}
|
||||
err := nanods.StoreCommandReport(nanoCtx, cmdRes)
|
||||
require.NoError(t, err)
|
||||
err = ds.NewActivity(ctx, nil, fleet.ActivityInstalledAppStoreApp{
|
||||
err = newActivityFn(ctx, nil, fleet.ActivityInstalledAppStoreApp{
|
||||
HostID: host.ID,
|
||||
AppStoreID: adamID,
|
||||
CommandUUID: execID,
|
||||
Status: "Error",
|
||||
}, []byte(`{}`), time.Now())
|
||||
Status: status,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
|
|
@ -180,9 +178,8 @@ func CreateHostInHouseAppInstallUpcomingActivity(t *testing.T, ds fleet.Datastor
|
|||
return execID
|
||||
}
|
||||
|
||||
func SetHostInHouseAppInstallResult(t *testing.T, ds fleet.Datastore, nanods storage.CommandAndReportResultsStore, host *fleet.Host, execID, status string) {
|
||||
func SetHostInHouseAppInstallResult(t *testing.T, ds fleet.Datastore, nanods storage.CommandAndReportResultsStore, host *fleet.Host, execID, status string, newActivityFn fleet.NewActivityFunc) {
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, fleet.ActivityWebhookContextKey, true)
|
||||
nanoCtx := &mdm.Request{EnrollID: &mdm.EnrollID{ID: host.UUID}, Context: ctx}
|
||||
|
||||
cmdRes := &mdm.CommandResults{
|
||||
|
|
@ -192,10 +189,10 @@ func SetHostInHouseAppInstallResult(t *testing.T, ds fleet.Datastore, nanods sto
|
|||
}
|
||||
err := nanods.StoreCommandReport(nanoCtx, cmdRes)
|
||||
require.NoError(t, err)
|
||||
err = ds.NewActivity(ctx, nil, fleet.ActivityTypeInstalledSoftware{
|
||||
err = newActivityFn(ctx, nil, fleet.ActivityTypeInstalledSoftware{
|
||||
HostID: host.ID,
|
||||
CommandUUID: execID,
|
||||
Status: "Error",
|
||||
}, []byte(`{}`), time.Now())
|
||||
Status: status,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,12 +14,12 @@ import (
|
|||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
|
||||
"github.com/fleetdm/fleet/v4/server/bindata"
|
||||
platformhttp "github.com/fleetdm/fleet/v4/server/platform/http"
|
||||
)
|
||||
|
||||
// GenerateRandomText return a string generated by filling in keySize bytes with
|
||||
|
|
@ -95,44 +95,15 @@ func PostJSONWithTimeout(ctx context.Context, url string, v interface{}) error {
|
|||
// MaskSecretURLParams masks URL query values if the query param name includes "secret", "token",
|
||||
// "key", "password". It accepts a raw string and returns a redacted string if the raw string is
|
||||
// URL-parseable. If it is not URL-parseable, the raw string is returned unchanged.
|
||||
func MaskSecretURLParams(rawURL string) string {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return rawURL
|
||||
}
|
||||
|
||||
keywords := []string{"secret", "token", "key", "password"}
|
||||
containsKeyword := func(s string) bool {
|
||||
s = strings.ToLower(s)
|
||||
for _, kw := range keywords {
|
||||
if strings.Contains(s, kw) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
q := u.Query()
|
||||
for k := range q {
|
||||
if containsKeyword(k) {
|
||||
q[k] = []string{"MASKED"}
|
||||
}
|
||||
}
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
return u.Redacted()
|
||||
}
|
||||
//
|
||||
// Deprecated: Use github.com/fleetdm/fleet/v4/server/platform/http.MaskSecretURLParams instead.
|
||||
var MaskSecretURLParams = platformhttp.MaskSecretURLParams
|
||||
|
||||
// MaskURLError checks if the provided error is a *url.Error. If so, it applies MaskSecretURLParams
|
||||
// to the URL value and returns the modified error. If not, the error is returned unchanged.
|
||||
func MaskURLError(e error) error {
|
||||
ue, ok := e.(*url.Error)
|
||||
if !ok {
|
||||
return e
|
||||
}
|
||||
ue.URL = MaskSecretURLParams(ue.URL)
|
||||
return ue
|
||||
}
|
||||
//
|
||||
// Deprecated: Use github.com/fleetdm/fleet/v4/server/platform/http.MaskURLError instead.
|
||||
var MaskURLError = platformhttp.MaskURLError
|
||||
|
||||
// TODO: Consider moving other crypto functions from server/mdm/apple/util to here
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue