diff --git a/changes/14722-activity-feed-webhooks b/changes/14722-activity-feed-webhooks new file mode 100644 index 00000000000..aa3f94a79a4 --- /dev/null +++ b/changes/14722-activity-feed-webhooks @@ -0,0 +1,2 @@ +Added `activities_webhook` configuration option to allow for a webhook to be called when an activity is recorded. This can be used to send activity data to external services. +If the webhook response is a 429 error code, the webhook retries for up to 30 minutes. diff --git a/cmd/fleetctl/apply_test.go b/cmd/fleetctl/apply_test.go index 82d1d8e0918..6ceb9824f97 100644 --- a/cmd/fleetctl/apply_test.go +++ b/cmd/fleetctl/apply_test.go @@ -173,7 +173,9 @@ func TestApplyTeamSpecs(t *testing.T) { return nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { return nil } @@ -545,7 +547,9 @@ func TestApplyAppConfig(t *testing.T) { return userRoleSpecList, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { return nil } @@ -705,7 +709,9 @@ func TestApplyAppConfigDryRunIssue(t *testing.T) { return userRoleSpecList[1], nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { return nil } @@ -1051,7 +1057,9 @@ func TestApplyPolicies(t *testing.T) { } return nil, errors.New("unexpected team name!") } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { return nil } @@ -1139,7 +1147,9 @@ 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) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { return nil } @@ -1664,7 +1674,9 @@ 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) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { return nil } @@ -1714,7 +1726,9 @@ func TestApplyQueries(t *testing.T) { appliedQueries = queries return nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { return nil } @@ -1819,7 +1833,9 @@ 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) error { + 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) { @@ -2585,7 +2601,9 @@ func TestApplySpecs(t *testing.T) { } // activities - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { return nil } @@ -3647,6 +3665,58 @@ spec: `, wantErr: `422 Validation Failed: Couldn't turn on Windows MDM. Please configure Fleet with a certificate and key pair first.`, }, + { + desc: "activities_webhook empty destination_url", + spec: ` +apiVersion: v1 +kind: config +spec: + webhook_settings: + activities_webhook: + enable_activities_webhook: true + destination_url: "" +`, + wantErr: `422 Validation Failed: destination_url is required`, + }, + { + desc: "activities_webhook bad destination_url 1", + spec: ` +apiVersion: v1 +kind: config +spec: + webhook_settings: + activities_webhook: + enable_activities_webhook: true + destination_url: ftp://host +`, + wantErr: `422 Validation Failed: destination_url must be http`, + }, + { + desc: "activities_webhook bad destination_url 2", + spec: ` +apiVersion: v1 +kind: config +spec: + webhook_settings: + activities_webhook: + enable_activities_webhook: true + destination_url: /foo +`, + wantErr: `422 Validation Failed: destination_url must be http`, + }, + { + desc: "activities_webhook bad destination_url 3", + spec: ` +apiVersion: v1 +kind: config +spec: + webhook_settings: + activities_webhook: + enable_activities_webhook: true + destination_url: foo +`, + wantErr: `422 Validation Failed: parse "foo": invalid URI`, + }, } // NOTE: Integrations required fields are not tested (Jira/Zendesk) because // they require a complex setup to mock the client that would communicate diff --git a/cmd/fleetctl/delete_test.go b/cmd/fleetctl/delete_test.go index 687cf203397..f3a90532444 100644 --- a/cmd/fleetctl/delete_test.go +++ b/cmd/fleetctl/delete_test.go @@ -3,6 +3,7 @@ package main import ( "context" "testing" + "time" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/stretchr/testify/assert" @@ -52,7 +53,9 @@ func TestDeletePack(t *testing.T) { Disabled: false, }, true, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { return nil } @@ -95,7 +98,9 @@ func TestDeleteQuery(t *testing.T) { ObserverCanRun: false, }, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { return nil } diff --git a/cmd/fleetctl/get_test.go b/cmd/fleetctl/get_test.go index 5fbc221fb75..3a8403bceae 100644 --- a/cmd/fleetctl/get_test.go +++ b/cmd/fleetctl/get_test.go @@ -2204,7 +2204,9 @@ 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) error { + 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) { diff --git a/cmd/fleetctl/gitops_test.go b/cmd/fleetctl/gitops_test.go index 36a354a9940..f4c3ff5f26a 100644 --- a/cmd/fleetctl/gitops_test.go +++ b/cmd/fleetctl/gitops_test.go @@ -46,7 +46,9 @@ func TestBasicGlobalGitOps(t *testing.T) { return nil } ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error { return nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + 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 } @@ -161,7 +163,9 @@ func TestBasicTeamGitOps(t *testing.T) { ) error { return nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { return nil } ds.ListTeamPoliciesFunc = func( @@ -274,7 +278,9 @@ func TestFullGlobalGitOps(t *testing.T) { appliedScripts = scripts return nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { return nil } var appliedMacProfiles []*fleet.MDMAppleConfigProfile @@ -406,6 +412,8 @@ func TestFullGlobalGitOps(t *testing.T) { assert.True(t, savedAppConfig.ActivityExpirySettings.ActivityExpiryEnabled) assert.Equal(t, 60, savedAppConfig.ActivityExpirySettings.ActivityExpiryWindow) assert.True(t, savedAppConfig.ServerSettings.AIFeaturesDisabled) + assert.True(t, savedAppConfig.WebhookSettings.ActivitiesWebhook.Enable) + assert.Equal(t, "https://activities_webhook_url", savedAppConfig.WebhookSettings.ActivitiesWebhook.DestinationURL) } func TestFullTeamGitOps(t *testing.T) { @@ -447,7 +455,9 @@ func TestFullTeamGitOps(t *testing.T) { appliedScripts = scripts return nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { return nil } var appliedMacProfiles []*fleet.MDMAppleConfigProfile @@ -720,7 +730,9 @@ func TestBasicGlobalAndTeamGitOps(t *testing.T) { return nil, nil } ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) { return nil, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + 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) { @@ -1062,7 +1074,9 @@ func setupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig, return nil, nil } ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) { return nil, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + 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) (*fleet.MDMAppleConfigProfile, error) { diff --git a/cmd/fleetctl/hosts_test.go b/cmd/fleetctl/hosts_test.go index bfedab996f4..a24fc79cbe3 100644 --- a/cmd/fleetctl/hosts_test.go +++ b/cmd/fleetctl/hosts_test.go @@ -3,6 +3,7 @@ package main import ( "context" "testing" + "time" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/stretchr/testify/assert" @@ -54,7 +55,9 @@ func TestHostsTransferByHosts(t *testing.T) { return &fleet.Team{ID: tid, Name: "team1"}, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { require.IsType(t, fleet.ActivityTypeTransferredHostsToTeam{}, activity) return nil } @@ -123,7 +126,9 @@ func TestHostsTransferByLabel(t *testing.T) { return &fleet.Team{ID: tid, Name: "team1"}, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { require.IsType(t, fleet.ActivityTypeTransferredHostsToTeam{}, activity) return nil } @@ -191,7 +196,9 @@ func TestHostsTransferByStatus(t *testing.T) { return &fleet.Team{ID: tid, Name: "team1"}, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { require.IsType(t, fleet.ActivityTypeTransferredHostsToTeam{}, activity) return nil } @@ -248,7 +255,9 @@ func TestHostsTransferByStatusAndSearchQuery(t *testing.T) { return &fleet.Team{ID: tid, Name: "team1"}, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { require.IsType(t, fleet.ActivityTypeTransferredHostsToTeam{}, activity) return nil } diff --git a/cmd/fleetctl/mdm_test.go b/cmd/fleetctl/mdm_test.go index e708c60decb..1145ff084ab 100644 --- a/cmd/fleetctl/mdm_test.go +++ b/cmd/fleetctl/mdm_test.go @@ -1276,7 +1276,9 @@ func setupDSMocks(ds *mock.Store, hostByUUID map[string]*fleet.Host, hostsByID m return h.MDMInfo, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { return nil } } diff --git a/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json b/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json index 3c8e19f5d94..7d0df637436 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json +++ b/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json @@ -1,132 +1,136 @@ { - "kind": "config", - "apiVersion": "v1", - "spec": { - "org_info": { - "org_name": "", - "org_logo_url": "", - "org_logo_url_light_background": "", - "contact_url": "https://fleetdm.com/company/contact" - }, - "server_settings": { - "server_url": "", - "live_query_disabled": false, - "query_reports_disabled": false, - "enable_analytics": false, - "deferred_save_host": false, - "scripts_disabled": false, - "ai_features_disabled": false - }, - "smtp_settings": { - "enable_smtp": false, - "configured": false, - "sender_address": "", - "server": "", - "port": 0, - "authentication_type": "", - "user_name": "", - "password": "", - "enable_ssl_tls": false, - "authentication_method": "", - "domain": "", - "verify_ssl_certs": false, - "enable_start_tls": false - }, - "host_expiry_settings": { - "host_expiry_enabled": false, - "host_expiry_window": 0 - }, - "activity_expiry_settings": { - "activity_expiry_enabled": false, - "activity_expiry_window": 0 - }, - "features": { - "enable_host_users": true, - "enable_software_inventory": false - }, - "sso_settings": { - "entity_id": "", - "issuer_uri": "", - "idp_image_url": "", - "metadata": "", - "metadata_url": "", - "idp_name": "", - "enable_jit_provisioning": false, - "enable_jit_role_sync": false, - "enable_sso": false, - "enable_sso_idp_login": false - }, - "fleet_desktop": { - "transparency_url": "https://fleetdm.com/transparency" - }, - "vulnerability_settings": { - "databases_path": "/some/path" - }, - "webhook_settings": { - "host_status_webhook": { - "enable_host_status_webhook": false, - "destination_url": "", - "host_percentage": 0, - "days_count": 0 - }, - "failing_policies_webhook": { - "enable_failing_policies_webhook": false, - "destination_url": "", - "policy_ids": null, - "host_batch_size": 0 - }, - "vulnerabilities_webhook": { - "enable_vulnerabilities_webhook": false, - "destination_url": "", - "host_batch_size": 0 - }, - "interval": "0s" - }, - "integrations": { - "jira": null, - "zendesk": null, - "google_calendar": null - }, - "mdm": { - "apple_bm_terms_expired": false, - "apple_bm_enabled_and_configured": false, - "enabled_and_configured": false, - "apple_bm_default_team": "", - "windows_enabled_and_configured": false, - "enable_disk_encryption": false, - "macos_updates": { - "minimum_version": null, - "deadline": null - }, - "windows_updates": { - "deadline_days": 7, - "grace_period_days": 3 - }, - "macos_migration": { - "enable": false, - "mode": "", - "webhook_url": "" - }, - "macos_settings": { - "custom_settings": null - }, - "macos_setup": { - "bootstrap_package": null, - "enable_end_user_authentication": false, - "macos_setup_assistant": null, - "enable_release_device_manually": false - }, - "windows_settings": { - "custom_settings": null - }, - "end_user_authentication": { - "entity_id": "", - "issuer_uri": "", - "metadata": "", - "metadata_url": "", - "idp_name": "" - } - }, - "scripts": null - } + "kind": "config", + "apiVersion": "v1", + "spec": { + "org_info": { + "org_name": "", + "org_logo_url": "", + "org_logo_url_light_background": "", + "contact_url": "https://fleetdm.com/company/contact" + }, + "server_settings": { + "server_url": "", + "live_query_disabled": false, + "query_reports_disabled": false, + "enable_analytics": false, + "deferred_save_host": false, + "scripts_disabled": false, + "ai_features_disabled": false + }, + "smtp_settings": { + "enable_smtp": false, + "configured": false, + "sender_address": "", + "server": "", + "port": 0, + "authentication_type": "", + "user_name": "", + "password": "", + "enable_ssl_tls": false, + "authentication_method": "", + "domain": "", + "verify_ssl_certs": false, + "enable_start_tls": false + }, + "host_expiry_settings": { + "host_expiry_enabled": false, + "host_expiry_window": 0 + }, + "activity_expiry_settings": { + "activity_expiry_enabled": false, + "activity_expiry_window": 0 + }, + "features": { + "enable_host_users": true, + "enable_software_inventory": false + }, + "sso_settings": { + "entity_id": "", + "issuer_uri": "", + "idp_image_url": "", + "metadata": "", + "metadata_url": "", + "idp_name": "", + "enable_jit_provisioning": false, + "enable_jit_role_sync": false, + "enable_sso": false, + "enable_sso_idp_login": false + }, + "fleet_desktop": { + "transparency_url": "https://fleetdm.com/transparency" + }, + "vulnerability_settings": { + "databases_path": "/some/path" + }, + "webhook_settings": { + "activities_webhook": { + "enable_activities_webhook": false, + "destination_url": "" + }, + "host_status_webhook": { + "enable_host_status_webhook": false, + "destination_url": "", + "host_percentage": 0, + "days_count": 0 + }, + "failing_policies_webhook": { + "enable_failing_policies_webhook": false, + "destination_url": "", + "policy_ids": null, + "host_batch_size": 0 + }, + "vulnerabilities_webhook": { + "enable_vulnerabilities_webhook": false, + "destination_url": "", + "host_batch_size": 0 + }, + "interval": "0s" + }, + "integrations": { + "jira": null, + "zendesk": null, + "google_calendar": null + }, + "mdm": { + "apple_bm_terms_expired": false, + "apple_bm_enabled_and_configured": false, + "enabled_and_configured": false, + "apple_bm_default_team": "", + "windows_enabled_and_configured": false, + "enable_disk_encryption": false, + "macos_updates": { + "minimum_version": null, + "deadline": null + }, + "windows_updates": { + "deadline_days": 7, + "grace_period_days": 3 + }, + "macos_migration": { + "enable": false, + "mode": "", + "webhook_url": "" + }, + "macos_settings": { + "custom_settings": null + }, + "macos_setup": { + "bootstrap_package": null, + "enable_end_user_authentication": false, + "macos_setup_assistant": null, + "enable_release_device_manually": false + }, + "windows_settings": { + "custom_settings": null + }, + "end_user_authentication": { + "entity_id": "", + "issuer_uri": "", + "metadata": "", + "metadata_url": "", + "idp_name": "" + } + }, + "scripts": null + } } diff --git a/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml b/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml index bfb7b77d75a..01834e56a57 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml +++ b/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml @@ -91,6 +91,9 @@ spec: vulnerability_settings: databases_path: /some/path webhook_settings: + activities_webhook: + enable_activities_webhook: false + destination_url: "" failing_policies_webhook: destination_url: "" enable_failing_policies_webhook: false diff --git a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json index b66af1c6df0..114ba52a9ca 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json +++ b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json @@ -1,194 +1,198 @@ { - "kind": "config", - "apiVersion": "v1", - "spec": { - "org_info": { - "org_name": "", - "org_logo_url": "", - "org_logo_url_light_background": "", - "contact_url": "https://fleetdm.com/company/contact" - }, - "server_settings": { - "server_url": "", - "live_query_disabled": false, - "query_reports_disabled": false, - "enable_analytics": false, - "deferred_save_host": false, - "scripts_disabled": false, - "ai_features_disabled": false - }, - "smtp_settings": { - "enable_smtp": false, - "configured": false, - "sender_address": "", - "server": "", - "port": 0, - "authentication_type": "", - "user_name": "", - "password": "", - "enable_ssl_tls": false, - "authentication_method": "", - "domain": "", - "verify_ssl_certs": false, - "enable_start_tls": false - }, - "host_expiry_settings": { - "host_expiry_enabled": false, - "host_expiry_window": 0 - }, - "activity_expiry_settings": { - "activity_expiry_enabled": false, - "activity_expiry_window": 0 - }, - "features": { - "enable_host_users": true, - "enable_software_inventory": false - }, - "mdm": { - "apple_bm_default_team": "", - "apple_bm_terms_expired": false, - "apple_bm_enabled_and_configured": false, - "enabled_and_configured": false, - "windows_enabled_and_configured": false, - "enable_disk_encryption": false, - "macos_updates": { - "minimum_version": null, - "deadline": null - }, - "windows_updates": { - "deadline_days": 7, - "grace_period_days": 3 - }, - "macos_migration": { - "enable": false, - "mode": "", - "webhook_url": "" - }, - "macos_settings": { - "custom_settings": null - }, - "macos_setup": { - "bootstrap_package": null, - "enable_end_user_authentication": false, - "macos_setup_assistant": null, - "enable_release_device_manually": false - }, - "windows_settings": { - "custom_settings": null - }, - "end_user_authentication": { - "entity_id": "", - "issuer_uri": "", - "metadata": "", - "metadata_url": "", - "idp_name": "" - } - }, - "scripts": null, - "sso_settings": { - "enable_jit_provisioning": false, - "enable_jit_role_sync": false, - "entity_id": "", - "issuer_uri": "", - "idp_image_url": "", - "metadata": "", - "metadata_url": "", - "idp_name": "", - "enable_sso": false, - "enable_sso_idp_login": false - }, - "fleet_desktop": { - "transparency_url": "https://fleetdm.com/transparency" - }, - "vulnerability_settings": { - "databases_path": "/some/path" - }, - "webhook_settings": { - "host_status_webhook": { - "enable_host_status_webhook": false, - "destination_url": "", - "host_percentage": 0, - "days_count": 0 - }, - "failing_policies_webhook": { - "enable_failing_policies_webhook": false, - "destination_url": "", - "policy_ids": null, - "host_batch_size": 0 - }, - "vulnerabilities_webhook": { - "enable_vulnerabilities_webhook": false, - "destination_url": "", - "host_batch_size": 0 - }, - "interval": "0s" - }, - "integrations": { - "jira": null, - "zendesk": null, - "google_calendar": null - }, - "update_interval": { - "osquery_detail": "1h0m0s", - "osquery_policy": "1h0m0s" - }, - "vulnerabilities": { - "databases_path": "", - "periodicity": "0s", - "cpe_database_url": "", - "cpe_translations_url": "", - "cve_feed_prefix_url": "", - "current_instance_checks": "", - "disable_data_sync": false, - "recent_vulnerability_max_age": "0s", - "disable_win_os_vulnerabilities": false - }, - "license": { - "tier": "free", - "expiration": "0001-01-01T00:00:00Z" - }, - "logging": { - "debug": true, - "json": false, - "result": { - "plugin": "filesystem", - "config": { - "enable_log_compression": false, - "enable_log_rotation": false, - "result_log_file": "/dev/null", - "status_log_file": "/dev/null", - "audit_log_file": "/dev/null", - "max_size": 500, - "max_age": 0, - "max_backups": 0 - } - }, - "status": { - "plugin": "filesystem", - "config": { - "enable_log_compression": false, - "enable_log_rotation": false, - "result_log_file": "/dev/null", - "status_log_file": "/dev/null", - "audit_log_file": "/dev/null", - "max_size": 500, - "max_age": 0, - "max_backups": 0 - } - }, - "audit": { - "plugin": "filesystem", - "config": { - "enable_log_compression": false, - "enable_log_rotation": false, - "result_log_file": "/dev/null", - "status_log_file": "/dev/null", - "audit_log_file": "/dev/null", - "max_size": 500, - "max_age": 0, - "max_backups": 0 - } - } - } - } + "kind": "config", + "apiVersion": "v1", + "spec": { + "org_info": { + "org_name": "", + "org_logo_url": "", + "org_logo_url_light_background": "", + "contact_url": "https://fleetdm.com/company/contact" + }, + "server_settings": { + "server_url": "", + "live_query_disabled": false, + "query_reports_disabled": false, + "enable_analytics": false, + "deferred_save_host": false, + "scripts_disabled": false, + "ai_features_disabled": false + }, + "smtp_settings": { + "enable_smtp": false, + "configured": false, + "sender_address": "", + "server": "", + "port": 0, + "authentication_type": "", + "user_name": "", + "password": "", + "enable_ssl_tls": false, + "authentication_method": "", + "domain": "", + "verify_ssl_certs": false, + "enable_start_tls": false + }, + "host_expiry_settings": { + "host_expiry_enabled": false, + "host_expiry_window": 0 + }, + "activity_expiry_settings": { + "activity_expiry_enabled": false, + "activity_expiry_window": 0 + }, + "features": { + "enable_host_users": true, + "enable_software_inventory": false + }, + "mdm": { + "apple_bm_default_team": "", + "apple_bm_terms_expired": false, + "apple_bm_enabled_and_configured": false, + "enabled_and_configured": false, + "windows_enabled_and_configured": false, + "enable_disk_encryption": false, + "macos_updates": { + "minimum_version": null, + "deadline": null + }, + "windows_updates": { + "deadline_days": 7, + "grace_period_days": 3 + }, + "macos_migration": { + "enable": false, + "mode": "", + "webhook_url": "" + }, + "macos_settings": { + "custom_settings": null + }, + "macos_setup": { + "bootstrap_package": null, + "enable_end_user_authentication": false, + "macos_setup_assistant": null, + "enable_release_device_manually": false + }, + "windows_settings": { + "custom_settings": null + }, + "end_user_authentication": { + "entity_id": "", + "issuer_uri": "", + "metadata": "", + "metadata_url": "", + "idp_name": "" + } + }, + "scripts": null, + "sso_settings": { + "enable_jit_provisioning": false, + "enable_jit_role_sync": false, + "entity_id": "", + "issuer_uri": "", + "idp_image_url": "", + "metadata": "", + "metadata_url": "", + "idp_name": "", + "enable_sso": false, + "enable_sso_idp_login": false + }, + "fleet_desktop": { + "transparency_url": "https://fleetdm.com/transparency" + }, + "vulnerability_settings": { + "databases_path": "/some/path" + }, + "webhook_settings": { + "activities_webhook": { + "enable_activities_webhook": false, + "destination_url": "" + }, + "host_status_webhook": { + "enable_host_status_webhook": false, + "destination_url": "", + "host_percentage": 0, + "days_count": 0 + }, + "failing_policies_webhook": { + "enable_failing_policies_webhook": false, + "destination_url": "", + "policy_ids": null, + "host_batch_size": 0 + }, + "vulnerabilities_webhook": { + "enable_vulnerabilities_webhook": false, + "destination_url": "", + "host_batch_size": 0 + }, + "interval": "0s" + }, + "integrations": { + "jira": null, + "zendesk": null, + "google_calendar": null + }, + "update_interval": { + "osquery_detail": "1h0m0s", + "osquery_policy": "1h0m0s" + }, + "vulnerabilities": { + "databases_path": "", + "periodicity": "0s", + "cpe_database_url": "", + "cpe_translations_url": "", + "cve_feed_prefix_url": "", + "current_instance_checks": "", + "disable_data_sync": false, + "recent_vulnerability_max_age": "0s", + "disable_win_os_vulnerabilities": false + }, + "license": { + "tier": "free", + "expiration": "0001-01-01T00:00:00Z" + }, + "logging": { + "debug": true, + "json": false, + "result": { + "plugin": "filesystem", + "config": { + "enable_log_compression": false, + "enable_log_rotation": false, + "result_log_file": "/dev/null", + "status_log_file": "/dev/null", + "audit_log_file": "/dev/null", + "max_size": 500, + "max_age": 0, + "max_backups": 0 + } + }, + "status": { + "plugin": "filesystem", + "config": { + "enable_log_compression": false, + "enable_log_rotation": false, + "result_log_file": "/dev/null", + "status_log_file": "/dev/null", + "audit_log_file": "/dev/null", + "max_size": 500, + "max_age": 0, + "max_backups": 0 + } + }, + "audit": { + "plugin": "filesystem", + "config": { + "enable_log_compression": false, + "enable_log_rotation": false, + "result_log_file": "/dev/null", + "status_log_file": "/dev/null", + "audit_log_file": "/dev/null", + "max_size": 500, + "max_age": 0, + "max_backups": 0 + } + } + } + } } diff --git a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml index ad517c69390..203246eb0dc 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml +++ b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml @@ -143,6 +143,9 @@ spec: vulnerability_settings: databases_path: /some/path webhook_settings: + activities_webhook: + enable_activities_webhook: false + destination_url: "" failing_policies_webhook: destination_url: "" enable_failing_policies_webhook: false diff --git a/cmd/fleetctl/testdata/gitops/global_config_no_paths.yml b/cmd/fleetctl/testdata/gitops/global_config_no_paths.yml index 60d41d888af..0a73e7392d6 100644 --- a/cmd/fleetctl/testdata/gitops/global_config_no_paths.yml +++ b/cmd/fleetctl/testdata/gitops/global_config_no_paths.yml @@ -153,6 +153,9 @@ org_settings: metadata: "" metadata_url: "" webhook_settings: + activities_webhook: + enable_activities_webhook: true + destination_url: https://activities_webhook_url failing_policies_webhook: destination_url: https://host.docker.internal:8080/bozo enable_failing_policies_webhook: false diff --git a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml index c629253b197..237ea64e3a0 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml @@ -91,6 +91,9 @@ spec: vulnerability_settings: databases_path: "" webhook_settings: + activities_webhook: + enable_activities_webhook: false + destination_url: "" failing_policies_webhook: destination_url: "" enable_failing_policies_webhook: false diff --git a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml index e33de77f96d..95b1be28ac4 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml @@ -91,6 +91,9 @@ spec: vulnerability_settings: databases_path: "" webhook_settings: + activities_webhook: + enable_activities_webhook: false + destination_url: "" failing_policies_webhook: destination_url: "" enable_failing_policies_webhook: false diff --git a/cmd/fleetctl/users_test.go b/cmd/fleetctl/users_test.go index d06910b798a..e65cc88d3a0 100644 --- a/cmd/fleetctl/users_test.go +++ b/cmd/fleetctl/users_test.go @@ -8,6 +8,7 @@ import ( "os" "strings" "testing" + "time" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/test" @@ -33,7 +34,9 @@ func TestUserDelete(t *testing.T) { deletedUser = id return nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { assert.Equal(t, fleet.ActivityTypeDeletedUser{}.ActivityName(), activity.ActivityName()) return nil } @@ -65,7 +68,9 @@ 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) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { return nil } @@ -123,7 +128,9 @@ 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) error { + 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) { @@ -154,7 +161,9 @@ func TestCreateBulkUsers(t *testing.T) { func TestDeleteBulkUsers(t *testing.T) { _, ds := runServerWithMockedDS(t) - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { return nil } csvFilePath := writeTmpCsv(t, diff --git a/ee/server/service/hosts.go b/ee/server/service/hosts.go index 4bd442516aa..e6833ee5a22 100644 --- a/ee/server/service/hosts.go +++ b/ee/server/service/hosts.go @@ -343,7 +343,7 @@ func (svc *Service) enqueueLockHostRequest(ctx context.Context, host *fleet.Host return ctxerr.Wrap(ctx, err, "enqueuing lock request for darwin") } - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, vc.User, fleet.ActivityTypeLockedHost{ @@ -377,7 +377,7 @@ func (svc *Service) enqueueLockHostRequest(ctx context.Context, host *fleet.Host return err } - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, vc.User, fleet.ActivityTypeLockedHost{ @@ -427,7 +427,7 @@ func (svc *Service) enqueueUnlockHostRequest(ctx context.Context, host *fleet.Ho } } - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, vc.User, fleet.ActivityTypeUnlockedHost{ @@ -482,7 +482,7 @@ func (svc *Service) enqueueWipeHostRequest(ctx context.Context, host *fleet.Host } } - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, vc.User, fleet.ActivityTypeWipedHost{ diff --git a/ee/server/service/mdm.go b/ee/server/service/mdm.go index 9889c599e93..985c573d9e1 100644 --- a/ee/server/service/mdm.go +++ b/ee/server/service/mdm.go @@ -268,7 +268,7 @@ func (svc *Service) updateMacOSSetupEnableEndUserAuth(ctx context.Context, enabl } else { act = fleet.ActivityTypeDisabledMacosSetupEndUserAuth{TeamID: teamID, TeamName: teamName} } - if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil { + if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil { return ctxerr.Wrap(ctx, err, "create activity for macos enable end user auth change") } return nil @@ -353,7 +353,10 @@ func (svc *Service) MDMAppleUploadBootstrapPackage(ctx context.Context, name str return err } - if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityTypeAddedBootstrapPackage{BootstrapPackageName: name, TeamID: ptrTeamId, TeamName: ptrTeamName}); err != nil { + if err := svc.NewActivity( + ctx, authz.UserFromContext(ctx), + fleet.ActivityTypeAddedBootstrapPackage{BootstrapPackageName: name, TeamID: ptrTeamId, TeamName: ptrTeamName}, + ); err != nil { return ctxerr.Wrap(ctx, err, "create activity for upload bootstrap package") } @@ -418,7 +421,10 @@ func (svc *Service) DeleteMDMAppleBootstrapPackage(ctx context.Context, teamID * return ctxerr.Wrap(ctx, err, "deleting bootstrap package") } - if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityTypeDeletedBootstrapPackage{BootstrapPackageName: meta.Name, TeamID: ptrTeamID, TeamName: ptrTeamName}); err != nil { + if err := svc.NewActivity( + ctx, authz.UserFromContext(ctx), + fleet.ActivityTypeDeletedBootstrapPackage{BootstrapPackageName: meta.Name, TeamID: ptrTeamID, TeamName: ptrTeamName}, + ); err != nil { return ctxerr.Wrap(ctx, err, "create activity for delete bootstrap package") } @@ -586,11 +592,12 @@ func (svc *Service) SetOrUpdateMDMAppleSetupAssistant(ctx context.Context, asst return nil, ctxerr.Wrap(ctx, err, "enqueue macos setup assistant profile changed job") } - if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeChangedMacosSetupAssistant{ - TeamID: newAsst.TeamID, - TeamName: teamName, - Name: newAsst.Name, - }); err != nil { + if err := svc.NewActivity( + ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeChangedMacosSetupAssistant{ + TeamID: newAsst.TeamID, + TeamName: teamName, + Name: newAsst.Name, + }); err != nil { return nil, ctxerr.Wrap(ctx, err, "create activity for changed macos setup assistant") } } @@ -638,11 +645,12 @@ func (svc *Service) DeleteMDMAppleSetupAssistant(ctx context.Context, teamID *ui } teamName = &tm.Name } - if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeDeletedMacosSetupAssistant{ - TeamID: teamID, - TeamName: teamName, - Name: prevAsst.Name, - }); err != nil { + if err := svc.NewActivity( + ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeDeletedMacosSetupAssistant{ + TeamID: teamID, + TeamName: teamName, + Name: prevAsst.Name, + }); err != nil { return ctxerr.Wrap(ctx, err, "create activity for deleted macos setup assistant") } } diff --git a/ee/server/service/mdm_external_test.go b/ee/server/service/mdm_external_test.go index e5bd1e49255..e61b1a07a43 100644 --- a/ee/server/service/mdm_external_test.go +++ b/ee/server/service/mdm_external_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "testing" + "time" "github.com/WatchBeam/clock" eeservice "github.com/fleetdm/fleet/v4/ee/server/service" @@ -145,7 +146,7 @@ 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) error { + 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) { diff --git a/ee/server/service/software_installers.go b/ee/server/service/software_installers.go index bb8e51234c0..bcd834a93d4 100644 --- a/ee/server/service/software_installers.go +++ b/ee/server/service/software_installers.go @@ -67,12 +67,13 @@ func (svc *Service) UploadSoftwareInstaller(ctx context.Context, payload *fleet. } // Create activity - if err := svc.ds.NewActivity(ctx, vc.User, fleet.ActivityTypeAddedSoftware{ - SoftwareTitle: payload.Title, - SoftwarePackage: payload.Filename, - TeamName: teamName, - TeamID: payload.TeamID, - }); err != nil { + if err := svc.NewActivity( + ctx, vc.User, fleet.ActivityTypeAddedSoftware{ + SoftwareTitle: payload.Title, + SoftwarePackage: payload.Filename, + TeamName: teamName, + TeamID: payload.TeamID, + }); err != nil { return ctxerr.Wrap(ctx, err, "creating activity for added software") } @@ -111,12 +112,13 @@ func (svc *Service) DeleteSoftwareInstaller(ctx context.Context, titleID uint, t teamName = &t.Name } - if err := svc.ds.NewActivity(ctx, vc.User, fleet.ActivityTypeDeletedSoftware{ - SoftwareTitle: meta.SoftwareTitle, - SoftwarePackage: meta.Name, - TeamName: teamName, - TeamID: meta.TeamID, - }); err != nil { + if err := svc.NewActivity( + ctx, vc.User, fleet.ActivityTypeDeletedSoftware{ + SoftwareTitle: meta.SoftwareTitle, + SoftwarePackage: meta.Name, + TeamName: teamName, + TeamID: meta.TeamID, + }); err != nil { return ctxerr.Wrap(ctx, err, "creating activity for deleted software") } diff --git a/ee/server/service/teams.go b/ee/server/service/teams.go index b19c7354682..ce175579411 100644 --- a/ee/server/service/teams.go +++ b/ee/server/service/teams.go @@ -101,7 +101,7 @@ func (svc *Service) NewTeam(ctx context.Context, p fleet.TeamPayload) (*fleet.Te return nil, err } - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeCreatedTeam{ @@ -253,7 +253,7 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T return nil, ctxerr.Wrap(ctx, err, "update DDM profile on macOS updates change") } - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeEditedMacOSMinVersion{ @@ -283,7 +283,7 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T return nil, ctxerr.Wrap(ctx, err, "disable team windows OS updates") } - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeEditedWindowsUpdates{ @@ -309,7 +309,7 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T return nil, ctxerr.Wrap(ctx, err, "disable team filevault and escrow") } } - if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil { + if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil { return nil, ctxerr.Wrap(ctx, err, "create activity for team macos disk encryption") } } @@ -357,7 +357,7 @@ func (svc *Service) ModifyTeamAgentOptions(ctx context.Context, teamID uint, tea return nil, err } - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeEditedAgentOptions{ @@ -572,7 +572,7 @@ func (svc *Service) DeleteTeam(ctx context.Context, teamID uint) error { logging.WithExtras(ctx, "id", teamID) - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeDeletedTeam{ @@ -833,7 +833,7 @@ func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec, } if !applyOpts.DryRun { - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeAppliedSpecTeam{ @@ -952,7 +952,7 @@ func (svc *Service) createTeamFromSpec( return nil, ctxerr.Wrap(ctx, err, "enable team filevault and escrow") } - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeEnabledMacosDiskEncryption{TeamID: &tm.ID, TeamName: &tm.Name}, @@ -1150,7 +1150,7 @@ func (svc *Service) editTeamFromSpec( return ctxerr.Wrap(ctx, err, "disable team filevault and escrow") } } - if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil { + if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil { return ctxerr.Wrap(ctx, err, "create activity for team macos disk encryption") } } @@ -1297,7 +1297,7 @@ func (svc *Service) updateTeamMDMDiskEncryption(ctx context.Context, tm *fleet.T return ctxerr.Wrap(ctx, err, "disable team filevault and escrow") } } - if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil { + if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil { return ctxerr.Wrap(ctx, err, "create activity for team macos disk encryption") } } diff --git a/ee/server/service/users.go b/ee/server/service/users.go index a5da9d0ae6e..15a413a16de 100644 --- a/ee/server/service/users.go +++ b/ee/server/service/users.go @@ -70,7 +70,7 @@ func (svc *Service) GetSSOUser(ctx context.Context, auth fleet.Auth) (*fleet.Use if err != nil { return nil, ctxerr.Wrap(ctx, err, "save user") } - if err := fleet.LogRoleChangeActivities(ctx, svc.ds, user, oldGlobalRole, oldTeamsRoles, user); err != nil { + if err := fleet.LogRoleChangeActivities(ctx, svc, user, oldGlobalRole, oldTeamsRoles, user); err != nil { return nil, ctxerr.Wrap(ctx, err, "log activities for role change") } return user, nil @@ -116,7 +116,7 @@ func (svc *Service) GetSSOUser(ctx context.Context, auth fleet.Auth) (*fleet.Use if err != nil { return nil, ctxerr.Wrap(ctx, err, "creating new SSO user") } - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, user, fleet.ActivityTypeUserAddedBySSO{}, diff --git a/server/datastore/mysql/activities.go b/server/datastore/mysql/activities.go index a37ece00d8f..7358ea08b41 100644 --- a/server/datastore/mysql/activities.go +++ b/server/datastore/mysql/activities.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "strings" + "time" "github.com/fleetdm/fleet/v4/pkg/scripts" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" @@ -15,10 +16,15 @@ import ( ) // NewActivity stores an activity item that the user performed -func (ds *Datastore) NewActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { - detailsBytes, err := json.Marshal(activity) - if err != nil { - return ctxerr.Wrap(ctx, err, "marshaling activity details") +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 @@ -30,12 +36,13 @@ func (ds *Datastore) NewActivity(ctx context.Context, user *fleet.User, activity userEmail = &user.Email } - cols := []string{"user_id", "user_name", "activity_type", "details"} + cols := []string{"user_id", "user_name", "activity_type", "details", "created_at"} args := []any{ userID, userName, activity.ActivityName(), - detailsBytes, + details, + createdAt, } if userEmail != nil { args = append(args, userEmail) diff --git a/server/datastore/mysql/activities_test.go b/server/datastore/mysql/activities_test.go index 5f4e9c03d5e..1df159b2971 100644 --- a/server/datastore/mysql/activities_test.go +++ b/server/datastore/mysql/activities_test.go @@ -82,14 +82,24 @@ func testActivityUsernameChange(t *testing.T, ds *Datastore) { _, err := ds.NewUser(context.Background(), u) require.NoError(t, err) - require.NoError(t, ds.NewActivity(context.Background(), u, dummyActivity{ - name: "test1", - details: map[string]interface{}{"detail": 1, "sometext": "aaa"}, - })) - require.NoError(t, ds.NewActivity(context.Background(), u, dummyActivity{ - name: "test2", - details: map[string]interface{}{"detail": 2}, - })) + timestamp := time.Now() + 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, + ), + ) activities, _, err := ds.ListActivities(context.Background(), fleet.ListActivitiesOptions{}) require.NoError(t, err) @@ -126,14 +136,35 @@ func testActivityNew(t *testing.T, ds *Datastore) { } _, err := ds.NewUser(context.Background(), u) require.Nil(t, err) - require.NoError(t, ds.NewActivity(context.Background(), u, dummyActivity{ - name: "test1", + timestamp := time.Now() + + activity := dummyActivity{ + name: "test0", details: map[string]interface{}{"detail": 1, "sometext": "aaa"}, - })) - require.NoError(t, ds.NewActivity(context.Background(), u, dummyActivity{ - name: "test2", - details: map[string]interface{}{"detail": 2}, - })) + } + // 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 := fleet.ListActivitiesOptions{ ListOptions: fleet.ListOptions{ @@ -180,18 +211,32 @@ func testListActivitiesStreamed(t *testing.T, ds *Datastore) { _, err := ds.NewUser(context.Background(), u) require.Nil(t, err) - require.NoError(t, ds.NewActivity(context.Background(), u, dummyActivity{ - name: "test1", - details: map[string]interface{}{"detail": 1, "sometext": "aaa"}, - })) - require.NoError(t, ds.NewActivity(context.Background(), u, dummyActivity{ - name: "test2", - details: map[string]interface{}{"detail": 2}, - })) - require.NoError(t, ds.NewActivity(context.Background(), u, dummyActivity{ - name: "test3", - details: map[string]interface{}{"detail": 3}, - })) + timestamp := time.Now() + 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, + ), + ) + require.NoError( + t, ds.NewActivity( + ctx, u, dummyActivity{ + name: "test3", + details: map[string]interface{}{"detail": 3}, + }, nil, timestamp, + ), + ) activities, _, err := ds.ListActivities(context.Background(), fleet.ListActivitiesOptions{}) require.NoError(t, err) @@ -229,21 +274,33 @@ func testListActivitiesStreamed(t *testing.T, ds *Datastore) { } func testActivityEmptyUser(t *testing.T, ds *Datastore) { - require.NoError(t, ds.NewActivity(context.Background(), nil, dummyActivity{ - name: "test1", - details: map[string]interface{}{"detail": 1, "sometext": "aaa"}, - })) + 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, + ), + ) activities, _, err := ds.ListActivities(context.Background(), fleet.ListActivitiesOptions{}) require.NoError(t, err) assert.Len(t, activities, 1) } func testActivityPaginationMetadata(t *testing.T, ds *Datastore) { + timestamp := time.Now() + ctx := context.WithValue(context.Background(), fleet.ActivityWebhookContextKey, true) for i := 0; i < 3; i++ { - require.NoError(t, ds.NewActivity(context.Background(), nil, dummyActivity{ - name: fmt.Sprintf("test-%d", i), - details: map[string]interface{}{}, - })) + require.NoError( + t, ds.NewActivity( + ctx, nil, dummyActivity{ + name: fmt.Sprintf("test-%d", i), + details: map[string]interface{}{}, + }, nil, timestamp, + ), + ) } cases := []struct { @@ -560,8 +617,6 @@ func testListHostUpcomingActivities(t *testing.T, ds *Datastore) { } func testListHostPastActivities(t *testing.T, ds *Datastore) { - ctx := context.Background() - getDetails := func(a *fleet.Activity) map[string]any { details := make(map[string]any) err := json.Unmarshal([]byte(*a.Details), &details) @@ -586,8 +641,12 @@ func testListHostPastActivities(t *testing.T, ds *Datastore) { }, } + timestamp := time.Now() + ctx := context.WithValue(context.Background(), fleet.ActivityWebhookContextKey, true) for _, a := range activities { - require.NoError(t, ds.NewActivity(context.Background(), u, a)) + detailsBytes, err := json.Marshal(a) + require.NoError(t, err) + require.NoError(t, ds.NewActivity(ctx, u, a, detailsBytes, timestamp)) } cases := []struct { @@ -694,27 +753,33 @@ 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{ name: "other activity", details: map[string]interface{}{"detail": 0, "foo": "zoo"}, - }) + }, nil, timestamp, + ) require.NoError(t, err) err = ds.NewActivity(ctx, user1, dummyActivity{ name: "live query", details: map[string]interface{}{"detail": 1, "foo": "bar"}, - }) + }, nil, timestamp, + ) require.NoError(t, err) err = ds.NewActivity(ctx, user1, 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{ 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. diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index 783fa156b72..a829e8ee89f 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -6593,13 +6593,20 @@ func testHostsDeleteHosts(t *testing.T, ds *Datastore) { `, host.UUID) require.NoError(t, err) + var activity fleet.ActivityDetails = fleet.ActivityTypeRanScript{ + HostID: host.ID, + HostDisplayName: host.DisplayName(), + } + detailsBytes, err := json.Marshal(activity) + require.NoError(t, err) + + ctx := context.WithValue(context.Background(), fleet.ActivityWebhookContextKey, true) err = ds.NewActivity( // automatically creates the host_activities entry - context.Background(), + ctx, user1, - fleet.ActivityTypeRanScript{ - HostID: host.ID, - HostDisplayName: host.DisplayName(), - }, + activity, + detailsBytes, + time.Now(), ) require.NoError(t, err) diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 791888aa104..a5654ba6863 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -41,7 +41,7 @@ CREATE TABLE `app_config_json` ( UNIQUE KEY `id` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"macos_setup\": {\"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false, \"enable_release_device_manually\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_default_team\": \"\", \"apple_bm_terms_expired\": false, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null, \"google_calendar\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"scripts_disabled\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"ai_features_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}, \"activity_expiry_settings\": {\"activity_expiry_window\": 0, \"activity_expiry_enabled\": false}}','2020-01-01 01:01:01','2020-01-01 01:01:01'); +INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"macos_setup\": {\"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false, \"enable_release_device_manually\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_default_team\": \"\", \"apple_bm_terms_expired\": false, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null, \"google_calendar\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"scripts_disabled\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"ai_features_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"activities_webhook\": {\"destination_url\": \"\", \"enable_activities_webhook\": false}, \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}, \"activity_expiry_settings\": {\"activity_expiry_window\": 0, \"activity_expiry_enabled\": false}}','2020-01-01 01:01:01','2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `calendar_events` ( diff --git a/server/fleet/activities.go b/server/fleet/activities.go index 5646802e462..cfd15960c8f 100644 --- a/server/fleet/activities.go +++ b/server/fleet/activities.go @@ -7,6 +7,11 @@ import ( //go:generate go run gen_activity_doc.go "../../docs/Using Fleet/Audit-logs.md" +type ContextKey string + +// ActivityWebhookContextKey is the context key to indicate that the activity webhook has been processed before saving the activity. +const ActivityWebhookContextKey = ContextKey("ActivityWebhook") + // ActivityDetailsList is used to generate documentation. var ActivityDetailsList = []ActivityDetails{ ActivityTypeCreatedPack{}, @@ -1503,9 +1508,11 @@ func (a ActivityTypeDeletedSoftware) Documentation() (string, string, string) { } // LogRoleChangeActivities logs activities for each role change, globally and one for each change in teams. -func LogRoleChangeActivities(ctx context.Context, ds Datastore, adminUser *User, oldGlobalRole *string, oldTeamRoles []UserTeam, user *User) error { +func LogRoleChangeActivities( + ctx context.Context, svc Service, adminUser *User, oldGlobalRole *string, oldTeamRoles []UserTeam, user *User, +) error { if user.GlobalRole != nil && (oldGlobalRole == nil || *oldGlobalRole != *user.GlobalRole) { - if err := ds.NewActivity( + if err := svc.NewActivity( ctx, adminUser, ActivityTypeChangedUserGlobalRole{ @@ -1519,7 +1526,7 @@ func LogRoleChangeActivities(ctx context.Context, ds Datastore, adminUser *User, } } if user.GlobalRole == nil && oldGlobalRole != nil { - if err := ds.NewActivity( + if err := svc.NewActivity( ctx, adminUser, ActivityTypeDeletedUserGlobalRole{ @@ -1544,7 +1551,7 @@ func LogRoleChangeActivities(ctx context.Context, ds Datastore, adminUser *User, if ok && o.Role == t.Role { continue } - if err := ds.NewActivity( + if err := svc.NewActivity( ctx, adminUser, ActivityTypeChangedUserTeamRole{ @@ -1563,7 +1570,7 @@ func LogRoleChangeActivities(ctx context.Context, ds Datastore, adminUser *User, if _, ok := newTeamsLookup[o.ID]; ok { continue } - if err := ds.NewActivity( + if err := svc.NewActivity( ctx, adminUser, ActivityTypeDeletedUserTeamRole{ diff --git a/server/fleet/app.go b/server/fleet/app.go index 6ea543252e8..e78de19d413 100644 --- a/server/fleet/app.go +++ b/server/fleet/app.go @@ -705,6 +705,7 @@ func (d *Duration) UnmarshalJSON(b []byte) error { } type WebhookSettings struct { + ActivitiesWebhook ActivitiesWebhookSettings `json:"activities_webhook"` HostStatusWebhook HostStatusWebhookSettings `json:"host_status_webhook"` FailingPoliciesWebhook FailingPoliciesWebhookSettings `json:"failing_policies_webhook"` VulnerabilitiesWebhook VulnerabilitiesWebhookSettings `json:"vulnerabilities_webhook"` @@ -714,6 +715,11 @@ type WebhookSettings struct { Interval Duration `json:"interval"` } +type ActivitiesWebhookSettings struct { + Enable bool `json:"enable_activities_webhook"` + DestinationURL string `json:"destination_url"` +} + type HostStatusWebhookSettings struct { Enable bool `json:"enable_host_status_webhook"` DestinationURL string `json:"destination_url"` diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 32cd2f14c4a..c32f9b8b926 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -591,7 +591,7 @@ type Datastore interface { /////////////////////////////////////////////////////////////////////////////// // ActivitiesStore - NewActivity(ctx context.Context, user *User, activity ActivityDetails) error + NewActivity(ctx context.Context, user *User, activity ActivityDetails, details []byte, createdAt time.Time) error ListActivities(ctx context.Context, opt ListActivitiesOptions) ([]*Activity, *PaginationMetadata, error) MarkActivitiesAsStreamed(ctx context.Context, activityIDs []uint) error ListHostUpcomingActivities(ctx context.Context, hostID uint, opt ListOptions) ([]*Activity, *PaginationMetadata, error) diff --git a/server/fleet/integrations.go b/server/fleet/integrations.go index 18cce718259..a4fab4c0f74 100644 --- a/server/fleet/integrations.go +++ b/server/fleet/integrations.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "net/url" "strconv" "strings" @@ -358,6 +359,24 @@ type Integrations struct { GoogleCalendar []*GoogleCalendarIntegration `json:"google_calendar"` } +func ValidateEnabledActivitiesWebhook(webhook ActivitiesWebhookSettings, invalid *InvalidArgumentError) { + if webhook.Enable { + if webhook.DestinationURL == "" { + invalid.Append( + "webhook_settings.activities_webhook.destination_url", "destination_url is required to enable the activities webhook", + ) + } else { + if u, err := url.ParseRequestURI(webhook.DestinationURL); err != nil { + invalid.Append("webhook_settings.activities_webhook.destination_url", err.Error()) + } else if (u.Scheme != "https" && u.Scheme != "http") || u.Host == "" { + invalid.Append( + "webhook_settings.activities_webhook.destination_url", "destination_url must be https or http, and have a host", + ) + } + } + } +} + // ValidateEnabledHostStatusIntegrations checks that the host status integrations // is properly configured if enabled. It adds any error it finds to the invalid // argument error, that can then be checked after the call for errors using diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index ee251dce6a1..a5eeabd09d9 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -419,7 +419,7 @@ type CleanupHostOperatingSystemsFunc func(ctx context.Context) error type MDMTurnOffFunc func(ctx context.Context, uuid string) error -type NewActivityFunc func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error +type NewActivityFunc func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error type ListActivitiesFunc func(ctx context.Context, opt fleet.ListActivitiesOptions) ([]*fleet.Activity, *fleet.PaginationMetadata, error) @@ -3758,11 +3758,11 @@ func (s *DataStore) MDMTurnOff(ctx context.Context, uuid string) error { return s.MDMTurnOffFunc(ctx, uuid) } -func (s *DataStore) NewActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { +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) + return s.NewActivityFunc(ctx, user, activity, details, createdAt) } func (s *DataStore) ListActivities(ctx context.Context, opt fleet.ListActivitiesOptions) ([]*fleet.Activity, *fleet.PaginationMetadata, error) { diff --git a/server/service/activities.go b/server/service/activities.go index b45e9936258..2e4aeac78f3 100644 --- a/server/service/activities.go +++ b/server/service/activities.go @@ -2,6 +2,16 @@ package service import ( "context" + "encoding/json" + "errors" + "fmt" + "github.com/cenkalti/backoff/v4" + "github.com/fleetdm/fleet/v4/server" + kithttp "github.com/go-kit/kit/transport/http" + kitlog "github.com/go-kit/log" + "github.com/go-kit/log/level" + "net/http" + "time" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" @@ -43,8 +53,78 @@ func (svc *Service) ListActivities(ctx context.Context, opt fleet.ListActivities return svc.ds.ListActivities(ctx, opt) } +type ActivityWebhookPayload 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"` +} + func (svc *Service) NewActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { - return svc.ds.NewActivity(ctx, user, activity) + return newActivity(ctx, user, activity, svc.ds, svc.logger) +} + +func newActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, ds fleet.Datastore, logger kitlog.Logger) error { + appConfig, err := ds.AppConfig(ctx) + if err != nil { + return ctxerr.Wrap(ctx, err, "get app config") + } + + detailsBytes, err := json.Marshal(activity) + if err != nil { + return ctxerr.Wrap(ctx, err, "marshaling activity details") + } + timestamp := time.Now() + + if appConfig.WebhookSettings.ActivitiesWebhook.Enable { + webhookURL := appConfig.WebhookSettings.ActivitiesWebhook.DestinationURL + var userID *uint + var userName *string + var userEmail *string + if user != nil { + userID = &user.ID + userName = &user.Name + userEmail = &user.Email + } + activityType := activity.ActivityName() + go func() { + retryStrategy := backoff.NewExponentialBackOff() + retryStrategy.MaxElapsedTime = 30 * time.Minute + err := backoff.Retry( + func() error { + if err := server.PostJSONWithTimeout( + context.Background(), webhookURL, &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) } //////////////////////////////////////////////////////////////////////////////// diff --git a/server/service/activities_test.go b/server/service/activities_test.go index 6240d81f1d0..d2042280ef6 100644 --- a/server/service/activities_test.go +++ b/server/service/activities_test.go @@ -2,16 +2,33 @@ package service import ( "context" + "encoding/json" + "net/http" + "net/http/httptest" "testing" + "time" "github.com/fleetdm/fleet/v4/server/authz" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mock" "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/test" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +type ActivityTypeTest struct { + Name string `json:"name"` +} + +func (a ActivityTypeTest) ActivityName() string { + return "test_activity" +} + +func (a ActivityTypeTest) Documentation() (activity string, details string, detailsExample string) { + return "test_activity", "test_activity", "test_activity" +} + func TestListActivities(t *testing.T) { ds := new(mock.Store) svc, ctx := newTestService(t, ds, nil, nil) @@ -104,13 +121,18 @@ func Test_logRoleChangeActivities(t *testing.T) { expectActivities: []string{"changed_user_team_role", "deleted_user_team_role"}, }, } - ctx := context.Background() ds := new(mock.Store) + svc, ctx := newTestService(t, ds, nil, nil) var activities []string - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { activities = append(activities, activity.ActivityName()) return nil } + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return &fleet.AppConfig{}, nil + } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { activities = activities[:0] @@ -132,8 +154,213 @@ func Test_logRoleChangeActivities(t *testing.T) { GlobalRole: tt.newRole, Teams: newTeams, } - require.NoError(t, fleet.LogRoleChangeActivities(ctx, ds, &fleet.User{}, tt.oldRole, oldTeams, newUser)) + require.NoError(t, fleet.LogRoleChangeActivities(ctx, svc, &fleet.User{}, tt.oldRole, oldTeams, newUser)) require.Equal(t, tt.expectActivities, activities) }) } } + +func TestActivityWebhooks(t *testing.T) { + ds := new(mock.Store) + svc, ctx := newTestService(t, ds, nil, nil) + var webhookBody = ActivityWebhookPayload{} + webhookChannel := make(chan struct{}, 1) + fail429 := false + startMockServer := func(t *testing.T) string { + // create a test http server + srv := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + webhookBody = ActivityWebhookPayload{} + if r.Method != "POST" { + w.WriteHeader(http.StatusMethodNotAllowed) + return // don't send the channel signal + } + switch r.URL.Path { + case "/ok": + err := json.NewDecoder(r.Body).Decode(&webhookBody) + if err != nil { + t.Log(err) + w.WriteHeader(http.StatusBadRequest) + } + case "/error": + webhookBody.Type = "error" // to check for testing + w.WriteHeader(http.StatusTeapot) + case "/429": + // Only the first request will fail + fail429 = !fail429 + if fail429 { + w.WriteHeader(http.StatusTooManyRequests) + return // don't send the channel signal + } + err := json.NewDecoder(r.Body).Decode(&webhookBody) + if err != nil { + t.Log(err) + w.WriteHeader(http.StatusBadRequest) + } + default: + w.WriteHeader(http.StatusNotFound) + return // don't send the channel signal + } + webhookChannel <- struct{}{} + }, + ), + ) + t.Cleanup(srv.Close) + return srv.URL + } + 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 + } + var activityUser *fleet.User + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) 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 + } + + tests := []struct { + name string + user *fleet.User + url string + doError bool + }{ + { + name: "nil user", + url: mockUrl + "/ok", + user: nil, + }, + { + name: "real user", + url: mockUrl + "/ok", + user: &fleet.User{ + ID: 1, + Name: "testUser", + Email: "testUser@example.com", + }, + }, + { + name: "error", + url: mockUrl + "/error", + doError: true, + }, + { + name: "429", + url: mockUrl + "/429", + user: &fleet.User{ + ID: 2, + Name: "testUser2", + Email: "testUser2@example.com", + }, + }, + } + + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + ds.NewActivityFuncInvoked = false + testUrl = tt.url + startTime := time.Now() + activity := ActivityTypeTest{Name: tt.name} + err := svc.NewActivity(ctx, tt.user, activity) + require.NoError(t, err) + select { + case <-time.After(1 * time.Second): + t.Error("timeout") + case <-webhookChannel: + if tt.doError { + assert.Equal(t, "error", webhookBody.Type) + } else { + endTime := time.Now() + assert.False( + t, webhookBody.Timestamp.Before(startTime), "timestamp %s is before start time %s", + webhookBody.Timestamp.String(), startTime.String(), + ) + assert.False(t, webhookBody.Timestamp.After(endTime)) + if tt.user == nil { + assert.Nil(t, webhookBody.ActorFullName) + assert.Nil(t, webhookBody.ActorID) + assert.Nil(t, webhookBody.ActorEmail) + } else { + require.NotNil(t, webhookBody.ActorFullName) + assert.Equal(t, tt.user.Name, *webhookBody.ActorFullName) + require.NotNil(t, webhookBody.ActorID) + assert.Equal(t, tt.user.ID, *webhookBody.ActorID) + require.NotNil(t, webhookBody.ActorEmail) + assert.Equal(t, tt.user.Email, *webhookBody.ActorEmail) + } + assert.Equal(t, activity.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) + }, + ) + } +} + +func TestActivityWebhooksDisabled(t *testing.T) { + ds := new(mock.Store) + svc, ctx := newTestService(t, ds, nil, nil) + startMockServer := func(t *testing.T) string { + // create a test http server + srv := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + t.Error("should not be called") + }, + ), + ) + t.Cleanup(srv.Close) + return srv.URL + } + mockUrl := startMockServer(t) + + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return &fleet.AppConfig{ + WebhookSettings: fleet.WebhookSettings{ + ActivitiesWebhook: fleet.ActivitiesWebhookSettings{ + Enable: false, + DestinationURL: mockUrl, + }, + }, + }, nil + } + var activityUser *fleet.User + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) 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"} + user := &fleet.User{ + ID: 1, + Name: "testUser", + Email: "testUser@example.com", + } + require.NoError(t, svc.NewActivity(ctx, user, activity)) + require.True(t, ds.NewActivityFuncInvoked) + assert.Equal(t, user, activityUser) +} diff --git a/server/service/appconfig.go b/server/service/appconfig.go index b2b0c0be125..f892fb13079 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -408,6 +408,7 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle fleet.ValidateEnabledVulnerabilitiesIntegrations(appConfig.WebhookSettings.VulnerabilitiesWebhook, appConfig.Integrations, invalid) fleet.ValidateEnabledFailingPoliciesIntegrations(appConfig.WebhookSettings.FailingPoliciesWebhook, appConfig.Integrations, invalid) fleet.ValidateEnabledHostStatusIntegrations(appConfig.WebhookSettings.HostStatusWebhook, invalid) + fleet.ValidateEnabledActivitiesWebhook(appConfig.WebhookSettings.ActivitiesWebhook, invalid) if err := svc.validateMDM(ctx, license, &oldAppConfig.MDM, &appConfig.MDM, invalid); err != nil { return nil, ctxerr.Wrap(ctx, err, "validating MDM config") } @@ -546,7 +547,7 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle newAgentOptions = string(*obfuscatedAppConfig.AgentOptions) } if oldAgentOptions != newAgentOptions { - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeEditedAgentOptions{ @@ -568,7 +569,7 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle } } - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeEditedMacOSMinVersion{ @@ -599,7 +600,7 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle return nil, ctxerr.Wrap(ctx, err, "disable no-team windows OS updates") } - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeEditedWindowsUpdates{ @@ -625,7 +626,7 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle return nil, ctxerr.Wrap(ctx, err, "disable no-team filevault and escrow") } } - if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil { + if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil { return nil, ctxerr.Wrap(ctx, err, "create activity for app config macos disk encryption") } } @@ -639,7 +640,7 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle } else { act = fleet.ActivityTypeDisabledMacosSetupEndUserAuth{} } - if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil { + if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil { return nil, ctxerr.Wrap(ctx, err, "create activity for macos enable end user auth change") } } @@ -661,7 +662,7 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle } else { act = fleet.ActivityTypeDisabledWindowsMDM{} } - if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil { + if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil { return nil, ctxerr.Wrapf(ctx, err, "create activity %s", act.ActivityName()) } } diff --git a/server/service/appconfig_test.go b/server/service/appconfig_test.go index 2e561eefca1..f6dd26b953b 100644 --- a/server/service/appconfig_test.go +++ b/server/service/appconfig_test.go @@ -13,6 +13,7 @@ import ( "os" "strings" "testing" + "time" "github.com/fleetdm/fleet/v4/pkg/optjson" "github.com/fleetdm/fleet/v4/server/config" @@ -1090,7 +1091,9 @@ func TestModifyAppConfigSMTPSSOAgentOptions(t *testing.T) { *dsAppConfig = *conf return nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { return nil } diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 94e77cf3247..8ac5fda6561 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -383,12 +383,13 @@ func (svc *Service) NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r actTeamID = &teamID actTeamName = &teamName } - if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeCreatedMacosProfile{ - TeamID: actTeamID, - TeamName: actTeamName, - ProfileName: newCP.Name, - ProfileIdentifier: newCP.Identifier, - }); err != nil { + if err := svc.NewActivity( + ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeCreatedMacosProfile{ + TeamID: actTeamID, + TeamName: actTeamName, + ProfileName: newCP.Name, + ProfileIdentifier: newCP.Identifier, + }); err != nil { return nil, ctxerr.Wrap(ctx, err, "logging activity for create mdm apple config profile") } @@ -469,12 +470,13 @@ func (svc *Service) NewMDMAppleDeclaration(ctx context.Context, teamID uint, r i actTeamID = &teamID actTeamName = &teamName } - if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeCreatedDeclarationProfile{ - TeamID: actTeamID, - TeamName: actTeamName, - ProfileName: decl.Name, - Identifier: decl.Identifier, - }); err != nil { + if err := svc.NewActivity( + ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeCreatedDeclarationProfile{ + TeamID: actTeamID, + TeamName: actTeamName, + ProfileName: decl.Name, + Identifier: decl.Identifier, + }); err != nil { return nil, ctxerr.Wrap(ctx, err, "logging activity for create mdm apple declaration") } @@ -771,12 +773,13 @@ func (svc *Service) DeleteMDMAppleConfigProfile(ctx context.Context, profileUUID actTeamID = &teamID actTeamName = &teamName } - if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeDeletedMacosProfile{ - TeamID: actTeamID, - TeamName: actTeamName, - ProfileName: cp.Name, - ProfileIdentifier: cp.Identifier, - }); err != nil { + if err := svc.NewActivity( + ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeDeletedMacosProfile{ + TeamID: actTeamID, + TeamName: actTeamName, + ProfileName: cp.Name, + ProfileIdentifier: cp.Identifier, + }); err != nil { return ctxerr.Wrap(ctx, err, "logging activity for delete mdm apple config profile") } @@ -850,12 +853,13 @@ func (svc *Service) DeleteMDMAppleDeclaration(ctx context.Context, declUUID stri actTeamID = &teamID actTeamName = &teamName } - if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeDeletedDeclarationProfile{ - TeamID: actTeamID, - TeamName: actTeamName, - ProfileName: decl.Name, - Identifier: decl.Identifier, - }); err != nil { + if err := svc.NewActivity( + ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeDeletedDeclarationProfile{ + TeamID: actTeamID, + TeamName: actTeamName, + ProfileName: decl.Name, + Identifier: decl.Identifier, + }); err != nil { return ctxerr.Wrap(ctx, err, "logging activity for delete mdm apple declaration") } @@ -1404,11 +1408,12 @@ func (svc *Service) EnqueueMDMAppleCommandRemoveEnrollmentProfile(ctx context.Co return ctxerr.Wrap(ctx, err, "enqueuing mdm apple remove profile command") } - if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeMDMUnenrolled{ - HostSerial: h.HardwareSerial, - HostDisplayName: h.DisplayName(), - InstalledFromDEP: info.InstalledFromDEP, - }); err != nil { + if err := svc.NewActivity( + ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeMDMUnenrolled{ + HostSerial: h.HardwareSerial, + HostDisplayName: h.DisplayName(), + InstalledFromDEP: info.InstalledFromDEP, + }); err != nil { return ctxerr.Wrap(ctx, err, "logging activity for mdm apple remove profile command") } @@ -1820,10 +1825,11 @@ func (svc *Service) BatchSetMDMAppleProfiles(ctx context.Context, tmID *uint, tm } } - if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeEditedMacosProfile{ - TeamID: tmID, - TeamName: tmName, - }); err != nil { + if err := svc.NewActivity( + ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeEditedMacosProfile{ + TeamID: tmID, + TeamName: tmName, + }); err != nil { return ctxerr.Wrap(ctx, err, "logging activity for edited macos profile") } return nil @@ -1955,7 +1961,7 @@ func (svc *Service) updateAppConfigMDMDiskEncryption(ctx context.Context, enable return ctxerr.Wrap(ctx, err, "disable no-team filevault and escrow") } } - if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil { + if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil { return ctxerr.Wrap(ctx, err, "create activity for app config macos disk encryption") } } @@ -2511,12 +2517,14 @@ func (svc *MDMAppleCheckinAndCommandService) Authenticate(r *mdm.Request, m *mdm return ctxerr.Wrap(r.Context, err, "getting checkin info in Authenticate message") } - return svc.ds.NewActivity(r.Context, nil, &fleet.ActivityTypeMDMEnrolled{ - HostSerial: updatedInfo.HardwareSerial, - HostDisplayName: updatedInfo.DisplayName, - InstalledFromDEP: updatedInfo.DEPAssignedToFleet, - MDMPlatform: fleet.MDMPlatformApple, - }) + return newActivity( + r.Context, nil, &fleet.ActivityTypeMDMEnrolled{ + HostSerial: updatedInfo.HardwareSerial, + HostDisplayName: updatedInfo.DisplayName, + InstalledFromDEP: updatedInfo.DEPAssignedToFleet, + MDMPlatform: fleet.MDMPlatformApple, + }, svc.ds, svc.logger, + ) } // TokenUpdate handles MDM [TokenUpdate][1] requests. @@ -2566,11 +2574,13 @@ func (svc *MDMAppleCheckinAndCommandService) CheckOut(r *mdm.Request, m *mdm.Che return err } - return svc.ds.NewActivity(r.Context, nil, &fleet.ActivityTypeMDMUnenrolled{ - HostSerial: info.HardwareSerial, - HostDisplayName: info.DisplayName, - InstalledFromDEP: info.InstalledFromDEP, - }) + return newActivity( + r.Context, nil, &fleet.ActivityTypeMDMUnenrolled{ + HostSerial: info.HardwareSerial, + HostDisplayName: info.DisplayName, + InstalledFromDEP: info.InstalledFromDEP, + }, svc.ds, svc.logger, + ) } // SetBootstrapToken handles MDM [SetBootstrapToken][1] requests. diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index 8be8d3dc9c6..f61b7fad24f 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -576,7 +576,7 @@ 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) error { + ds.NewActivityFunc = func(context.Context, *fleet.User, fleet.ActivityDetails, []byte, time.Time) error { return nil } ds.GetMDMAppleProfilesSummaryFunc = func(context.Context, *uint) (*fleet.MDMProfilesSummary, error) { @@ -686,7 +686,7 @@ func TestNewMDMAppleConfigProfile(t *testing.T) { require.Equal(t, mcBytes, []byte(cp.Mobileconfig)) return &cp, nil } - ds.NewActivityFunc = func(context.Context, *fleet.User, fleet.ActivityDetails) error { + 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) error { @@ -896,7 +896,7 @@ func TestMDMCommandAuthz(t *testing.T) { return &fleet.HostMDMCheckinInfo{}, nil } - ds.NewActivityFunc = func(context.Context, *fleet.User, fleet.ActivityDetails) error { + ds.NewActivityFunc = func(context.Context, *fleet.User, fleet.ActivityDetails, []byte, time.Time) error { return nil } ds.MDMTurnOffFunc = func(ctx context.Context, uuid string) error { @@ -1037,7 +1037,12 @@ func TestMDMAuthenticateManualEnrollment(t *testing.T) { }, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + 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, + ) error { a, ok := activity.(*fleet.ActivityTypeMDMEnrolled) require.True(t, ok) require.Nil(t, user) @@ -1097,7 +1102,12 @@ func TestMDMAuthenticateADE(t *testing.T) { }, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + 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, + ) error { a, ok := activity.(*fleet.ActivityTypeMDMEnrolled) require.True(t, ok) require.Nil(t, user) @@ -1151,7 +1161,9 @@ func TestMDMAuthenticateSCEPRenewal(t *testing.T) { }, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + 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) error { @@ -1275,7 +1287,12 @@ func TestMDMCheckout(t *testing.T) { }, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + 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, + ) error { a, ok := activity.(*fleet.ActivityTypeMDMUnenrolled) require.True(t, ok) require.Nil(t, user) @@ -1455,7 +1472,9 @@ 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) error { + 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) error { @@ -1769,7 +1788,9 @@ 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) error { + 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) error { @@ -1806,7 +1827,9 @@ 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) error { + 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) { @@ -1960,7 +1983,9 @@ func TestUpdateMDMAppleSetup(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) error { + 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) { @@ -2653,7 +2678,9 @@ func TestEnsureFleetdConfig(t *testing.T) { func TestMDMAppleSetupAssistant(t *testing.T) { svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium}) - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + 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) { diff --git a/server/service/global_policies.go b/server/service/global_policies.go index 98ae2117189..c7d03e96954 100644 --- a/server/service/global_policies.go +++ b/server/service/global_policies.go @@ -77,7 +77,7 @@ func (svc Service) NewGlobalPolicy(ctx context.Context, p fleet.PolicyPayload) ( } // Note: Issue #4191 proposes that we move to SQL transactions for actions so that we can // rollback an action in the event of an error writing the associated activity - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeCreatedPolicy{ @@ -253,7 +253,7 @@ func (svc Service) DeleteGlobalPolicies(ctx context.Context, ids []uint) ([]uint // Note: Issue #4191 proposes that we move to SQL transactions for actions so that we can // rollback an action in the event of an error writing the associated activity for _, id := range deletedIDs { - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeDeletedPolicy{ @@ -546,7 +546,7 @@ func (svc *Service) ApplyPolicySpecs(ctx context.Context, policies []*fleet.Poli } // Note: Issue #4191 proposes that we move to SQL transactions for actions so that we can // rollback an action in the event of an error writing the associated activity - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeAppliedSpecPolicy{ diff --git a/server/service/global_policies_test.go b/server/service/global_policies_test.go index 108de7c70b0..db6ddacc661 100644 --- a/server/service/global_policies_test.go +++ b/server/service/global_policies_test.go @@ -3,6 +3,7 @@ package service import ( "context" "testing" + "time" "github.com/fleetdm/fleet/v4/server/contexts/viewer" "github.com/fleetdm/fleet/v4/server/fleet" @@ -65,7 +66,9 @@ 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) error { + 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 { diff --git a/server/service/global_schedule_test.go b/server/service/global_schedule_test.go index d34707d46a3..4656ca2df48 100644 --- a/server/service/global_schedule_test.go +++ b/server/service/global_schedule_test.go @@ -3,6 +3,7 @@ package service import ( "context" "testing" + "time" "github.com/fleetdm/fleet/v4/server/contexts/viewer" "github.com/fleetdm/fleet/v4/server/fleet" @@ -27,7 +28,12 @@ func TestGlobalScheduleAuth(t *testing.T) { ds.SaveQueryFunc = func(ctx context.Context, query *fleet.Query, shouldDiscardResults bool, shouldDeleteStats bool) error { return nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + 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, error) { diff --git a/server/service/hosts.go b/server/service/hosts.go index 021bc058c53..e75e8ccba41 100644 --- a/server/service/hosts.go +++ b/server/service/hosts.go @@ -865,7 +865,7 @@ func (svc *Service) createTransferredHostsActivity(ctx context.Context, teamID * } } - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeTransferredHostsToTeam{ @@ -2145,7 +2145,7 @@ func (svc *Service) HostEncryptionKey(ctx context.Context, id uint) (*fleet.Host } key.DecryptedValue = string(decryptedKey) - err = svc.ds.NewActivity( + err = svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeReadHostDiskEncryptionKey{ diff --git a/server/service/hosts_test.go b/server/service/hosts_test.go index 49a824b335c..8284dfc9b72 100644 --- a/server/service/hosts_test.go +++ b/server/service/hosts_test.go @@ -603,7 +603,7 @@ func TestHostAuth(t *testing.T) { ds.TeamFunc = func(ctx context.Context, id uint) (*fleet.Team, error) { return &fleet.Team{ID: id}, nil } - ds.NewActivityFunc = func(ctx context.Context, u *fleet.User, a fleet.ActivityDetails) error { + 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) { @@ -858,6 +858,9 @@ func TestAddHostsToTeamByFilter(t *testing.T) { expectedHostIDs := []uint{1, 2, 4} expectedTeam := (*uint)(nil) + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return &fleet.AppConfig{}, nil + } ds.ListHostsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.HostListOptions) ([]*fleet.Host, error) { var hosts []*fleet.Host for _, id := range expectedHostIDs { @@ -876,7 +879,9 @@ 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) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { return nil } @@ -895,6 +900,9 @@ func TestAddHostsToTeamByFilterLabel(t *testing.T) { expectedTeam := ptr.Uint(1) expectedLabel := float64(2) + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return &fleet.AppConfig{}, nil + } ds.ListHostsInLabelFunc = func(ctx context.Context, filter fleet.TeamFilter, lid uint, opt fleet.HostListOptions) ([]*fleet.Host, error) { assert.Equal(t, uint(expectedLabel), lid) var hosts []*fleet.Host @@ -916,7 +924,9 @@ func TestAddHostsToTeamByFilterLabel(t *testing.T) { ds.TeamFunc = func(ctx context.Context, id uint) (*fleet.Team, error) { return &fleet.Team{ID: id}, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { return nil } @@ -1251,7 +1261,9 @@ func TestHostEncryptionKey(t *testing.T) { }, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { act := activity.(fleet.ActivityTypeReadHostDiskEncryptionKey) require.Equal(t, tt.host.ID, act.HostID) require.EqualValues(t, act.HostDisplayName, tt.host.DisplayName()) @@ -1309,7 +1321,9 @@ func TestHostEncryptionKey(t *testing.T) { return &fleet.HostDiskEncryptionKey{Base64Encrypted: "key"}, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { return errors.New("activity error") } @@ -1350,7 +1364,9 @@ func TestHostEncryptionKey(t *testing.T) { Decryptable: ptr.Bool(true), }, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { return nil } @@ -1532,7 +1548,9 @@ 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) error { + 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 { diff --git a/server/service/http_auth_test.go b/server/service/http_auth_test.go index f88bccdd014..065ac314662 100644 --- a/server/service/http_auth_test.go +++ b/server/service/http_auth_test.go @@ -171,7 +171,12 @@ func setupAuthTest(t *testing.T) (fleet.Datastore, map[string]fleet.User, *httpt sessions[sessionKey] = session return session, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + 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 diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index d5f60a7e937..2ee1f6bcaf7 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -2840,13 +2840,15 @@ func (s *integrationTestSuite) TestListActivities() { prevActivities, _, err := s.ds.ListActivities(ctx, fleet.ListActivitiesOptions{}) require.NoError(t, err) - err = s.ds.NewActivity(ctx, &u, fleet.ActivityTypeAppliedSpecPack{}) + timestamp := time.Now() + ctx = context.WithValue(ctx, fleet.ActivityWebhookContextKey, true) + err = s.ds.NewActivity(ctx, &u, fleet.ActivityTypeAppliedSpecPack{}, nil, timestamp) require.NoError(t, err) - err = s.ds.NewActivity(ctx, &u, fleet.ActivityTypeDeletedPack{}) + err = s.ds.NewActivity(ctx, &u, fleet.ActivityTypeDeletedPack{}, nil, timestamp) require.NoError(t, err) - err = s.ds.NewActivity(ctx, &u, fleet.ActivityTypeEditedPack{}) + err = s.ds.NewActivity(ctx, &u, fleet.ActivityTypeEditedPack{}, nil, timestamp) require.NoError(t, err) lenPage := len(prevActivities) + 2 @@ -4582,6 +4584,27 @@ func (s *integrationTestSuite) TestGlobalPoliciesAutomationConfig() { require.Empty(t, config.WebhookSettings.FailingPoliciesWebhook.PolicyIDs) } +func (s *integrationTestSuite) TestActivitiesWebhookConfig() { + t := s.T() + + s.DoRaw( + "PATCH", "/api/latest/fleet/config", []byte( + `{ + "webhook_settings": { + "activities_webhook": { + "enable_activities_webhook": true, + "destination_url": "http://some/url" + } + } + }`, + ), http.StatusOK, + ) + + appConfig := s.getConfig() + require.True(t, appConfig.WebhookSettings.ActivitiesWebhook.Enable) + require.Equal(t, "http://some/url", appConfig.WebhookSettings.ActivitiesWebhook.DestinationURL) +} + func (s *integrationTestSuite) TestHostStatusWebhookConfig() { t := s.T() diff --git a/server/service/mdm.go b/server/service/mdm.go index ef47f609ca5..adbd2c0c13b 100644 --- a/server/service/mdm.go +++ b/server/service/mdm.go @@ -1162,11 +1162,12 @@ func (svc *Service) DeleteMDMWindowsConfigProfile(ctx context.Context, profileUU actTeamID = &teamID actTeamName = &teamName } - if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeDeletedWindowsProfile{ - TeamID: actTeamID, - TeamName: actTeamName, - ProfileName: prof.Name, - }); err != nil { + if err := svc.NewActivity( + ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeDeletedWindowsProfile{ + TeamID: actTeamID, + TeamName: actTeamName, + ProfileName: prof.Name, + }); err != nil { return ctxerr.Wrap(ctx, err, "logging activity for delete mdm windows config profile") } @@ -1371,11 +1372,12 @@ func (svc *Service) NewMDMWindowsConfigProfile(ctx context.Context, teamID uint, actTeamID = &teamID actTeamName = &teamName } - if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeCreatedWindowsProfile{ - TeamID: actTeamID, - TeamName: actTeamName, - ProfileName: newCP.Name, - }); err != nil { + if err := svc.NewActivity( + ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeCreatedWindowsProfile{ + TeamID: actTeamID, + TeamName: actTeamName, + ProfileName: newCP.Name, + }); err != nil { return nil, ctxerr.Wrap(ctx, err, "logging activity for create mdm windows config profile") } @@ -1562,22 +1564,25 @@ func (svc *Service) BatchSetMDMProfiles( // TODO(roberto): should we generate activities only of any profiles were // changed? this is the existing behavior for macOS profiles so I'm // leaving it as-is for now. - if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeEditedMacosProfile{ - TeamID: tmID, - TeamName: tmName, - }); err != nil { + if err := svc.NewActivity( + ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeEditedMacosProfile{ + TeamID: tmID, + TeamName: tmName, + }); err != nil { return ctxerr.Wrap(ctx, err, "logging activity for edited macos profile") } - if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeEditedWindowsProfile{ - TeamID: tmID, - TeamName: tmName, - }); err != nil { + if err := svc.NewActivity( + ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeEditedWindowsProfile{ + TeamID: tmID, + TeamName: tmName, + }); err != nil { return ctxerr.Wrap(ctx, err, "logging activity for edited windows profile") } - if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeEditedDeclarationProfile{ - TeamID: tmID, - TeamName: tmName, - }); err != nil { + if err := svc.NewActivity( + ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeEditedDeclarationProfile{ + TeamID: tmID, + TeamName: tmName, + }); err != nil { return ctxerr.Wrap(ctx, err, "logging activity for edited macos declarations") } @@ -2099,11 +2104,12 @@ func (svc *Service) ResendHostMDMProfile(ctx context.Context, hostID uint, profi return ctxerr.Wrap(ctx, err, "resending host mdm profile") } - if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeResentConfigurationProfile{ - HostID: &host.ID, - HostDisplayName: ptr.String(host.DisplayName()), - ProfileName: profileName, - }); err != nil { + if err := svc.NewActivity( + ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeResentConfigurationProfile{ + HostID: &host.ID, + HostDisplayName: ptr.String(host.DisplayName()), + ProfileName: profileName, + }); err != nil { return ctxerr.Wrap(ctx, err, "logging activity for resend config profile") } diff --git a/server/service/mdm_test.go b/server/service/mdm_test.go index a5c9635b185..04fe3c42bcb 100644 --- a/server/service/mdm_test.go +++ b/server/service/mdm_test.go @@ -913,7 +913,7 @@ func TestMDMWindowsConfigProfileAuthz(t *testing.T) { }, }, nil } - ds.NewActivityFunc = func(context.Context, *fleet.User, fleet.ActivityDetails) error { + 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) { @@ -1002,7 +1002,7 @@ func TestUploadWindowsMDMConfigProfileValidations(t *testing.T) { } return &fleet.Team{ID: tid, Name: "team1"}, nil } - ds.NewActivityFunc = func(context.Context, *fleet.User, fleet.ActivityDetails) error { + ds.NewActivityFunc = func(context.Context, *fleet.User, fleet.ActivityDetails, []byte, time.Time) error { return nil } ds.NewMDMWindowsConfigProfileFunc = func(ctx context.Context, cp fleet.MDMWindowsConfigProfile) (*fleet.MDMWindowsConfigProfile, error) { @@ -1098,7 +1098,9 @@ func TestMDMBatchSetProfiles(t *testing.T) { ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration) error { return nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + 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) error { @@ -1708,7 +1710,7 @@ 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) error { + ds.NewActivityFunc = func(context.Context, *fleet.User, fleet.ActivityDetails, []byte, time.Time) error { return nil } diff --git a/server/service/microsoft_mdm.go b/server/service/microsoft_mdm.go index e8a44ae0ae3..5b8ebe67fde 100644 --- a/server/service/microsoft_mdm.go +++ b/server/service/microsoft_mdm.go @@ -1777,10 +1777,11 @@ func (svc *Service) storeWindowsMDMEnrolledDevice(ctx context.Context, userID st } } - err = svc.ds.NewActivity(ctx, nil, &fleet.ActivityTypeMDMEnrolled{ - HostDisplayName: reqDeviceName, - MDMPlatform: fleet.MDMPlatformMicrosoft, - }) + err = svc.NewActivity( + ctx, nil, &fleet.ActivityTypeMDMEnrolled{ + HostDisplayName: reqDeviceName, + MDMPlatform: fleet.MDMPlatformMicrosoft, + }) if err != nil { // only logging, the device is enrolled at this point, and we // wouldn't want to fail the request because there was a problem diff --git a/server/service/orbit.go b/server/service/orbit.go index 3581957078a..8cfb8f43aa0 100644 --- a/server/service/orbit.go +++ b/server/service/orbit.go @@ -625,7 +625,7 @@ func (svc *Service) SaveHostScriptResult(ctx context.Context, result *fleet.Host } // TODO(sarah): We may need to special case lock/unlock script results here? - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, user, fleet.ActivityTypeRanScript{ @@ -917,7 +917,7 @@ func (svc *Service) SaveHostSoftwareInstallResult(ctx context.Context, result *f } } - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, user, fleet.ActivityTypeInstalledSoftware{ diff --git a/server/service/osquery_test.go b/server/service/osquery_test.go index 35d2d596271..860ec89f8bb 100644 --- a/server/service/osquery_test.go +++ b/server/service/osquery_test.go @@ -2954,7 +2954,9 @@ 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) error { + 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}}) @@ -2988,7 +2990,9 @@ 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) error { + 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) @@ -3028,7 +3032,9 @@ 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) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { return nil } // var gotQuery *fleet.Query @@ -3046,7 +3052,9 @@ 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) error { + 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) diff --git a/server/service/packs.go b/server/service/packs.go index aaef3335229..8533a2f3667 100644 --- a/server/service/packs.go +++ b/server/service/packs.go @@ -214,7 +214,7 @@ func (svc *Service) NewPack(ctx context.Context, p fleet.PackPayload) (*fleet.Pa return nil, err } - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeCreatedPack{ @@ -310,7 +310,7 @@ func (svc *Service) ModifyPack(ctx context.Context, id uint, p fleet.PackPayload return nil, err } - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeEditedPack{ @@ -406,7 +406,7 @@ func (svc *Service) DeletePack(ctx context.Context, name string) error { return err } - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeDeletedPack{ @@ -457,7 +457,7 @@ func (svc *Service) DeletePackByID(ctx context.Context, id uint) error { return err } - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeDeletedPack{ @@ -536,7 +536,7 @@ func (svc *Service) ApplyPackSpecs(ctx context.Context, specs []*fleet.PackSpec) return nil, err } - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeAppliedSpecPack{}, diff --git a/server/service/packs_test.go b/server/service/packs_test.go index b07e67e22aa..29c16b38e5f 100644 --- a/server/service/packs_test.go +++ b/server/service/packs_test.go @@ -3,6 +3,7 @@ package service import ( "context" "testing" + "time" "github.com/fleetdm/fleet/v4/server/authz" "github.com/fleetdm/fleet/v4/server/contexts/viewer" @@ -42,7 +43,12 @@ func TestNewPackSavesTargets(t *testing.T) { ds.NewPackFunc = func(ctx context.Context, pack *fleet.Pack, opts ...fleet.OptionalArg) (*fleet.Pack, error) { return pack, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + 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 } diff --git a/server/service/queries.go b/server/service/queries.go index 988f3d232b0..dcfe1a455eb 100644 --- a/server/service/queries.go +++ b/server/service/queries.go @@ -287,7 +287,7 @@ func (svc *Service) NewQuery(ctx context.Context, p fleet.QueryPayload) (*fleet. return nil, err } - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeCreatedSavedQuery{ @@ -402,7 +402,7 @@ func (svc *Service) ModifyQuery(ctx context.Context, id uint, p fleet.QueryPaylo return nil, err } - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeEditedSavedQuery{ @@ -471,7 +471,7 @@ func (svc *Service) DeleteQuery(ctx context.Context, teamID *uint, name string) return err } - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeDeletedSavedQuery{ @@ -521,7 +521,7 @@ func (svc *Service) DeleteQueryByID(ctx context.Context, id uint) error { return ctxerr.Wrap(ctx, err, "delete query") } - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeDeletedSavedQuery{ @@ -575,7 +575,7 @@ func (svc *Service) DeleteQueries(ctx context.Context, ids []uint) (uint, error) return n, err } - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeDeletedMultipleSavedQuery{ @@ -665,7 +665,7 @@ func (svc *Service) ApplyQuerySpecs(ctx context.Context, specs []*fleet.QuerySpe return ctxerr.Wrap(ctx, err, "applying queries") } - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeAppliedSpecSavedQuery{ diff --git a/server/service/queries_test.go b/server/service/queries_test.go index 9b9cdfb1c9b..fc8631264b8 100644 --- a/server/service/queries_test.go +++ b/server/service/queries_test.go @@ -19,7 +19,12 @@ func TestQueryPayloadValidationCreate(t *testing.T) { ds.NewQueryFunc = func(ctx context.Context, query *fleet.Query, opts ...fleet.OptionalArg) (*fleet.Query, error) { return query, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + 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 { act, ok := activity.(fleet.ActivityTypeCreatedSavedQuery) assert.True(t, ok) assert.NotEmpty(t, act.Name) @@ -144,7 +149,12 @@ func TestQueryPayloadValidationModify(t *testing.T) { return nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + 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 { act, ok := activity.(fleet.ActivityTypeEditedSavedQuery) assert.True(t, ok) assert.NotEmpty(t, act.Name) @@ -358,7 +368,12 @@ func TestQueryAuth(t *testing.T) { } return nil, newNotFoundError() } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + 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) { diff --git a/server/service/scripts.go b/server/service/scripts.go index 0017648a4bb..bde49918679 100644 --- a/server/service/scripts.go +++ b/server/service/scripts.go @@ -524,7 +524,7 @@ func (svc *Service) NewScript(ctx context.Context, teamID *uint, name string, r teamName = &tm.Name } - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeAddedScript{ @@ -582,7 +582,7 @@ func (svc *Service) DeleteScript(ctx context.Context, scriptID uint) error { teamName = &tm.Name } - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeDeletedScript{ @@ -865,10 +865,11 @@ func (svc *Service) BatchSetScripts(ctx context.Context, maybeTmID *uint, maybeT return ctxerr.Wrap(ctx, err, "batch saving scripts") } - if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeEditedScript{ - TeamID: teamID, - TeamName: teamName, - }); err != nil { + if err := svc.NewActivity( + ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeEditedScript{ + TeamID: teamID, + TeamName: teamName, + }); err != nil { return ctxerr.Wrap(ctx, err, "logging activity for edited scripts") } return nil diff --git a/server/service/scripts_test.go b/server/service/scripts_test.go index 98d7e2dfb85..b12e4cf5a65 100644 --- a/server/service/scripts_test.go +++ b/server/service/scripts_test.go @@ -527,7 +527,9 @@ 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) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { return nil } ds.TeamFunc = func(ctx context.Context, id uint) (*fleet.Team, error) { diff --git a/server/service/service_campaign_test.go b/server/service/service_campaign_test.go index d33a8f69dd1..17c9a993e5a 100644 --- a/server/service/service_campaign_test.go +++ b/server/service/service_campaign_test.go @@ -60,7 +60,9 @@ 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) error { + 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) { diff --git a/server/service/service_campaigns.go b/server/service/service_campaigns.go index 615f0543c0f..6970e4ee928 100644 --- a/server/service/service_campaigns.go +++ b/server/service/service_campaigns.go @@ -273,7 +273,7 @@ func (svc Service) addLiveQueryActivity( activityData.Stats = &q.AggregatedStats } } - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), activityData, diff --git a/server/service/service_users.go b/server/service/service_users.go index ae30b531d07..2c830632e3d 100644 --- a/server/service/service_users.go +++ b/server/service/service_users.go @@ -54,7 +54,7 @@ func (svc *Service) NewUser(ctx context.Context, p fleet.UserPayload) (*fleet.Us // In case of invites the user created herself. adminUser = user } - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, adminUser, fleet.ActivityTypeCreatedUser{ @@ -65,7 +65,7 @@ func (svc *Service) NewUser(ctx context.Context, p fleet.UserPayload) (*fleet.Us ); err != nil { return nil, err } - if err := fleet.LogRoleChangeActivities(ctx, svc.ds, adminUser, nil, nil, user); err != nil { + if err := fleet.LogRoleChangeActivities(ctx, svc, adminUser, nil, nil, user); err != nil { return nil, err } diff --git a/server/service/sessions.go b/server/service/sessions.go index da46ef3041a..a1f2fe36ab4 100644 --- a/server/service/sessions.go +++ b/server/service/sessions.go @@ -166,10 +166,11 @@ func (svc *Service) Login(ctx context.Context, email, password string) (*fleet.U var err error defer func(start time.Time) { if err != nil { - if err := svc.ds.NewActivity(ctx, nil, fleet.ActivityTypeUserFailedLogin{ - Email: email, - PublicIP: publicip.FromContext(ctx), - }); err != nil { + if err := svc.NewActivity( + ctx, nil, fleet.ActivityTypeUserFailedLogin{ + Email: email, + PublicIP: publicip.FromContext(ctx), + }); err != nil { logging.WithExtras(logging.WithNoUser(ctx), "msg", "failed to generate failed login activity", ) @@ -200,9 +201,10 @@ func (svc *Service) Login(ctx context.Context, email, password string) (*fleet.U return nil, nil, fleet.NewAuthFailedError(err.Error()) } - if err := svc.ds.NewActivity(ctx, user, fleet.ActivityTypeUserLoggedIn{ - PublicIP: publicip.FromContext(ctx), - }); err != nil { + if err := svc.NewActivity( + ctx, user, fleet.ActivityTypeUserLoggedIn{ + PublicIP: publicip.FromContext(ctx), + }); err != nil { return nil, nil, err } return user, session, nil @@ -518,7 +520,7 @@ func (svc *Service) LoginSSOUser(ctx context.Context, user *fleet.User, redirect Token: session.Key, RedirectURL: redirectURL, } - err = svc.ds.NewActivity( + err = svc.NewActivity( ctx, user, fleet.ActivityTypeUserLoggedIn{ diff --git a/server/service/sessions_test.go b/server/service/sessions_test.go index 139cacfebe7..b35820ce603 100644 --- a/server/service/sessions_test.go +++ b/server/service/sessions_test.go @@ -219,7 +219,9 @@ func TestGetSSOUser(t *testing.T) { }, }) - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { return nil } diff --git a/server/service/software_installers_test.go b/server/service/software_installers_test.go index d58b41d9633..e70050b766d 100644 --- a/server/service/software_installers_test.go +++ b/server/service/software_installers_test.go @@ -68,7 +68,12 @@ func TestSoftwareInstallersAuth(t *testing.T) { return nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + 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 } diff --git a/server/service/team_policies.go b/server/service/team_policies.go index a2698c145ed..62540aef942 100644 --- a/server/service/team_policies.go +++ b/server/service/team_policies.go @@ -82,7 +82,7 @@ func (svc Service) NewTeamPolicy(ctx context.Context, teamID uint, p fleet.Polic // Note: Issue #4191 proposes that we move to SQL transactions for actions so that we can // rollback an action in the event of an error writing the associated activity - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeCreatedPolicy{ @@ -303,7 +303,7 @@ func (svc Service) DeleteTeamPolicies(ctx context.Context, teamID uint, ids []ui // Note: Issue #4191 proposes that we move to SQL transactions for actions so that we can // rollback an action in the event of an error writing the associated activity for _, id := range deletedIDs { - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeDeletedPolicy{ @@ -422,7 +422,7 @@ func (svc *Service) modifyPolicy(ctx context.Context, teamID *uint, id uint, p f // Note: Issue #4191 proposes that we move to SQL transactions for actions so that we can // rollback an action in the event of an error writing the associated activity - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityTypeEditedPolicy{ diff --git a/server/service/team_policies_test.go b/server/service/team_policies_test.go index 6a2d35d4ff6..5c6c25ff8ce 100644 --- a/server/service/team_policies_test.go +++ b/server/service/team_policies_test.go @@ -3,6 +3,7 @@ package service import ( "context" "testing" + "time" "github.com/fleetdm/fleet/v4/server/authz" "github.com/fleetdm/fleet/v4/server/contexts/viewer" @@ -56,7 +57,12 @@ func TestTeamPoliciesAuth(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) error { + 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.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) { diff --git a/server/service/team_schedule_test.go b/server/service/team_schedule_test.go index e348a95fe66..f6a52cdadbd 100644 --- a/server/service/team_schedule_test.go +++ b/server/service/team_schedule_test.go @@ -3,6 +3,7 @@ package service import ( "context" "testing" + "time" "github.com/fleetdm/fleet/v4/server/contexts/viewer" "github.com/fleetdm/fleet/v4/server/fleet" @@ -36,7 +37,12 @@ func TestTeamScheduleAuth(t *testing.T) { ds.SaveQueryFunc = func(ctx context.Context, query *fleet.Query, shouldDiscardResults bool, shouldDeleteStats bool) error { return nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + 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) { diff --git a/server/service/teams_test.go b/server/service/teams_test.go index 136e396697e..7d2fac3a53b 100644 --- a/server/service/teams_test.go +++ b/server/service/teams_test.go @@ -26,7 +26,9 @@ 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) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { return nil } ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) { @@ -275,7 +277,9 @@ func TestApplyTeamSpecs(t *testing.T) { return team, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { act := activity.(fleet.ActivityTypeAppliedSpecTeam) require.Len(t, act.Teams, 1) return nil @@ -356,7 +360,9 @@ func TestApplyTeamSpecs(t *testing.T) { return &fleet.Team{ID: 123}, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { act := activity.(fleet.ActivityTypeAppliedSpecTeam) require.Len(t, act.Teams, 1) return nil @@ -389,7 +395,9 @@ func TestApplyTeamSpecEnrollSecretForNewTeams(t *testing.T) { return &fleet.AppConfig{}, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { return nil } diff --git a/server/service/users.go b/server/service/users.go index 5196598d118..795fbb02d66 100644 --- a/server/service/users.go +++ b/server/service/users.go @@ -419,7 +419,7 @@ func (svc *Service) ModifyUser(ctx context.Context, userID uint, p fleet.UserPay return nil, err } adminUser := authz.UserFromContext(ctx) - if err := fleet.LogRoleChangeActivities(ctx, svc.ds, adminUser, oldGlobalRole, oldTeams, user); err != nil { + if err := fleet.LogRoleChangeActivities(ctx, svc, adminUser, oldGlobalRole, oldTeams, user); err != nil { return nil, err } @@ -463,7 +463,7 @@ func (svc *Service) DeleteUser(ctx context.Context, id uint) error { } adminUser := authz.UserFromContext(ctx) - if err := svc.ds.NewActivity( + if err := svc.NewActivity( ctx, adminUser, fleet.ActivityTypeDeletedUser{ diff --git a/server/service/users_test.go b/server/service/users_test.go index 23c44ae359c..d47e1bacd5b 100644 --- a/server/service/users_test.go +++ b/server/service/users_test.go @@ -86,7 +86,12 @@ func TestUserAuth(t *testing.T) { ds.ListSessionsForUserFunc = func(ctx context.Context, id uint) ([]*fleet.Session, error) { return nil, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + 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 } diff --git a/tools/cloner-check/generated_files/appconfig.txt b/tools/cloner-check/generated_files/appconfig.txt index f07c2831ddc..3babb180732 100644 --- a/tools/cloner-check/generated_files/appconfig.txt +++ b/tools/cloner-check/generated_files/appconfig.txt @@ -57,6 +57,9 @@ github.com/fleetdm/fleet/v4/server/fleet/FleetDesktopSettings TransparencyURL st github.com/fleetdm/fleet/v4/server/fleet/AppConfig VulnerabilitySettings fleet.VulnerabilitySettings github.com/fleetdm/fleet/v4/server/fleet/VulnerabilitySettings DatabasesPath string github.com/fleetdm/fleet/v4/server/fleet/AppConfig WebhookSettings fleet.WebhookSettings +github.com/fleetdm/fleet/v4/server/fleet/WebhookSettings ActivitiesWebhook fleet.ActivitiesWebhookSettings +github.com/fleetdm/fleet/v4/server/fleet/ActivitiesWebhookSettings Enable bool +github.com/fleetdm/fleet/v4/server/fleet/ActivitiesWebhookSettings DestinationURL string github.com/fleetdm/fleet/v4/server/fleet/WebhookSettings HostStatusWebhook fleet.HostStatusWebhookSettings github.com/fleetdm/fleet/v4/server/fleet/HostStatusWebhookSettings Enable bool github.com/fleetdm/fleet/v4/server/fleet/HostStatusWebhookSettings DestinationURL string