From c805ea215442a4363305262f41ef3ef41ef9353b Mon Sep 17 00:00:00 2001 From: Martin Angers Date: Tue, 31 Jan 2023 17:36:18 -0500 Subject: [PATCH] Record activity when the macos minimum version requirement is edited (#9594) --- changes/issue-9355-macos-min-version-activity | 1 + docs/Using-Fleet/Audit-Activities.md | 49 +++++-- docs/Using-Fleet/Detail-Queries-Summary.md | 10 +- ee/server/service/teams.go | 25 +++- server/fleet/activities.go | 57 ++++++--- server/service/appconfig.go | 17 ++- server/service/integration_enterprise_test.go | 121 +++++++++++++++--- 7 files changed, 229 insertions(+), 51 deletions(-) create mode 100644 changes/issue-9355-macos-min-version-activity diff --git a/changes/issue-9355-macos-min-version-activity b/changes/issue-9355-macos-min-version-activity new file mode 100644 index 0000000000..ccc254f99e --- /dev/null +++ b/changes/issue-9355-macos-min-version-activity @@ -0,0 +1 @@ +* Added an activity `edited_macos_min_version` when the required minimum macOS version is updated. diff --git a/docs/Using-Fleet/Audit-Activities.md b/docs/Using-Fleet/Audit-Activities.md index 6966938ea3..6f8211d4d9 100644 --- a/docs/Using-Fleet/Audit-Activities.md +++ b/docs/Using-Fleet/Audit-Activities.md @@ -43,7 +43,7 @@ This activity contains the following fields: ```json { - "pack_id": 123, + "pack_id": 123, "pack_name": "foo" } ``` @@ -60,7 +60,7 @@ This activity contains the following fields: ```json { - "pack_id": 123, + "pack_id": 123, "pack_name": "foo" } ``` @@ -98,7 +98,7 @@ This activity contains the following fields: ```json { - "policy_id": 123, + "policy_id": 123, "policy_name": "foo" } ``` @@ -115,7 +115,7 @@ This activity contains the following fields: ```json { - "policy_id": 123, + "policy_id": 123, "policy_name": "foo" } ``` @@ -132,7 +132,7 @@ This activity contains the following fields: ```json { - "policy_id": 123, + "policy_id": 123, "policy_name": "foo" } ``` @@ -188,7 +188,7 @@ This activity contains the following fields: ```json { - "query_id": 123, + "query_id": 123, "query_name": "foo" } ``` @@ -205,7 +205,7 @@ This activity contains the following fields: ```json { - "query_id": 123, + "query_id": 123, "query_name": "foo" } ``` @@ -275,7 +275,7 @@ This activity contains the following fields: ```json { - "team_id": 123, + "team_id": 123, "team_name": "foo" } ``` @@ -292,7 +292,7 @@ This activity contains the following fields: ```json { - "team_id": 123, + "team_id": 123, "team_name": "foo" } ``` @@ -311,7 +311,7 @@ This activity contains a field "teams" where each item contains the team details { "teams": [ { - "id": 123, + "id": 123, "name": "foo" } ] @@ -331,7 +331,7 @@ This activity contains the following fields: ```json { - "team_id": 123, + "team_id": 123, "team_name": "foo", "global": false } @@ -350,7 +350,7 @@ This activity contains the following fields: ```json { - "targets_count": 5000, + "targets_count": 5000, "query_sql": "SELECT * from osquery_info;", "query_name": "foo" } @@ -530,6 +530,7 @@ Generated when a host is enrolled in Fleet's MDM. This activity contains the following fields: - "host_serial": Serial number of the host. +- "host_display_name": Display name of the host. - "installed_from_dep": Whether the host was enrolled via DEP. #### Example @@ -537,6 +538,7 @@ This activity contains the following fields: ```json { "host_serial": "C08VQ2AXHT96", + "host_display_name": "MacBookPro16,1 (C08VQ2AXHT96)", "installed_from_dep": true } ``` @@ -547,6 +549,7 @@ Generated when a host is unenrolled from Fleet's MDM. This activity contains the following fields: - "host_serial": Serial number of the host. +- "host_display_name": Display name of the host. - "installed_from_dep": Whether the host was enrolled via DEP. #### Example @@ -554,10 +557,32 @@ This activity contains the following fields: ```json { "host_serial": "C08VQ2AXHT96", + "host_display_name": "MacBookPro16,1 (C08VQ2AXHT96)", "installed_from_dep": true } ``` +### Type `edited_macos_min_version` + +Generated when the minimum required macOS version is modified. + +This activity contains the following fields: +- "team_id": The ID of the team that the minimum macOS version applies to, null if it applies to devices that are not in a team. +- "team_name": The name of the team that the minimum macOS version applies to, null if it applies to devices that are not in a team. +- "minimum_version": The minimum macOS version required, empty if the requirement was removed. +- "deadline": The deadline by which the minimum version requirement must be applied, empty if the requirement was removed. + +#### Example + +```json +{ + "team_id": 3, + "team_name": "Workstations", + "minimum_version": "13.0.1", + "deadline": "2023-06-01" +} +``` + \ No newline at end of file diff --git a/docs/Using-Fleet/Detail-Queries-Summary.md b/docs/Using-Fleet/Detail-Queries-Summary.md index d13ad66f63..a5fcd91dde 100644 --- a/docs/Using-Fleet/Detail-Queries-Summary.md +++ b/docs/Using-Fleet/Detail-Queries-Summary.md @@ -25,7 +25,7 @@ SELECT 1 FROM disk_encryption WHERE user_uuid IS NOT "" AND filevault_status = ' ## disk_encryption_linux -- Platforms: linux, ubuntu, debian, rhel, centos, sles, kali, gentoo, amzn, pop, arch, linuxmint, void +- Platforms: linux, ubuntu, debian, rhel, centos, sles, kali, gentoo, amzn, pop, arch, linuxmint, void, nixos - Query: @@ -45,7 +45,7 @@ SELECT 1 FROM bitlocker_info WHERE drive_letter = 'C:' AND protection_status = 1 ## disk_space_unix -- Platforms: linux, ubuntu, debian, rhel, centos, sles, kali, gentoo, amzn, pop, arch, linuxmint, void, darwin +- Platforms: linux, ubuntu, debian, rhel, centos, sles, kali, gentoo, amzn, pop, arch, linuxmint, void, nixos, darwin - Query: @@ -166,7 +166,7 @@ select version, errors, warnings from munki_info; ## network_interface_unix -- Platforms: linux, ubuntu, debian, rhel, centos, sles, kali, gentoo, amzn, pop, arch, linuxmint, void, darwin +- Platforms: linux, ubuntu, debian, rhel, centos, sles, kali, gentoo, amzn, pop, arch, linuxmint, void, nixos, darwin - Query: @@ -230,7 +230,7 @@ SELECT version FROM orbit_info ## os_unix_like -- Platforms: linux, ubuntu, debian, rhel, centos, sles, kali, gentoo, amzn, pop, arch, linuxmint, void, darwin +- Platforms: linux, ubuntu, debian, rhel, centos, sles, kali, gentoo, amzn, pop, arch, linuxmint, void, nixos, darwin - Query: @@ -328,7 +328,7 @@ SELECT *, ## software_linux -- Platforms: linux, ubuntu, debian, rhel, centos, sles, kali, gentoo, amzn, pop, arch, linuxmint, void +- Platforms: linux, ubuntu, debian, rhel, centos, sles, kali, gentoo, amzn, pop, arch, linuxmint, void, nixos - Query: diff --git a/ee/server/service/teams.go b/ee/server/service/teams.go index cbfb42d893..621855066d 100644 --- a/ee/server/service/teams.go +++ b/ee/server/service/teams.go @@ -103,10 +103,12 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T team.Config.WebhookSettings = *payload.WebhookSettings } + var macOSMinVersionUpdated bool if payload.MDM != nil { if err := payload.MDM.MacOSUpdates.Validate(); err != nil { return nil, fleet.NewInvalidArgumentError("macos_updates", err.Error()) } + macOSMinVersionUpdated = team.Config.MDM.MacOSUpdates != payload.MDM.MacOSUpdates team.Config.MDM = *payload.MDM } @@ -144,7 +146,25 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T } } - return svc.ds.SaveTeam(ctx, team) + team, err = svc.ds.SaveTeam(ctx, team) + if err != nil { + return nil, err + } + if macOSMinVersionUpdated { + if err := svc.ds.NewActivity( + ctx, + authz.UserFromContext(ctx), + fleet.ActivityTypeEditedMacOSMinVersion{ + TeamID: &team.ID, + TeamName: &team.Name, + MinimumVersion: team.Config.MDM.MacOSUpdates.MinimumVersion, + Deadline: team.Config.MDM.MacOSUpdates.Deadline, + }, + ); err != nil { + return nil, ctxerr.Wrap(ctx, err, "create activity for team macos min version edited") + } + } + return team, err } func (svc *Service) ModifyTeamAgentOptions(ctx context.Context, teamID uint, teamOptions json.RawMessage, applyOptions fleet.ApplySpecOptions) (*fleet.Team, error) { @@ -496,6 +516,9 @@ func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec, } if len(details) > 0 { + // TODO(mna): we don't create separate activities for e.g. edited agent + // options when applying team specs, so I didn't create explicit activities + // for min macos version either. Not sure if that's what we want. if err := svc.ds.NewActivity( ctx, authz.UserFromContext(ctx), diff --git a/server/fleet/activities.go b/server/fleet/activities.go index 6d7d38e9d1..0b5b7e1177 100644 --- a/server/fleet/activities.go +++ b/server/fleet/activities.go @@ -45,6 +45,8 @@ var ActivityDetailsList = []ActivityDetails{ ActivityTypeMDMEnrolled{}, ActivityTypeMDMUnenrolled{}, + + ActivityTypeEditedMacOSMinVersion{}, } type ActivityDetails interface { @@ -68,7 +70,7 @@ func (a ActivityTypeCreatedPack) Documentation() (activity string, details strin `This activity contains the following fields: - "pack_id": the id of the created pack. - "pack_name": the name of the created pack.`, `{ - "pack_id": 123, + "pack_id": 123, "pack_name": "foo" }` } @@ -87,7 +89,7 @@ func (a ActivityTypeEditedPack) Documentation() (activity string, details string `This activity contains the following fields: - "pack_id": the id of the edited pack. - "pack_name": the name of the edited pack.`, `{ - "pack_id": 123, + "pack_id": 123, "pack_name": "foo" }` } @@ -133,7 +135,7 @@ func (a ActivityTypeCreatedPolicy) Documentation() (activity string, details str `This activity contains the following fields: - "policy_id": the ID of the created policy. - "policy_name": the name of the created policy.`, `{ - "policy_id": 123, + "policy_id": 123, "policy_name": "foo" }` } @@ -152,7 +154,7 @@ func (a ActivityTypeEditedPolicy) Documentation() (activity string, details stri `This activity contains the following fields: - "policy_id": the ID of the edited policy. - "policy_name": the name of the edited policy.`, `{ - "policy_id": 123, + "policy_id": 123, "policy_name": "foo" }` } @@ -171,7 +173,7 @@ func (a ActivityTypeDeletedPolicy) Documentation() (activity string, details str `This activity contains the following fields: - "policy_id": the ID of the deleted policy. - "policy_name": the name of the deleted policy.`, `{ - "policy_id": 123, + "policy_id": 123, "policy_name": "foo" }` } @@ -230,7 +232,7 @@ func (a ActivityTypeCreatedSavedQuery) Documentation() (activity string, details `This activity contains the following fields: - "query_id": the ID of the created query. - "query_name": the name of the created query.`, `{ - "query_id": 123, + "query_id": 123, "query_name": "foo" }` } @@ -249,7 +251,7 @@ func (a ActivityTypeEditedSavedQuery) Documentation() (activity string, details `This activity contains the following fields: - "query_id": the ID of the query being edited. - "query_name": the name of the query being edited.`, `{ - "query_id": 123, + "query_id": 123, "query_name": "foo" }` } @@ -324,7 +326,7 @@ func (a ActivityTypeCreatedTeam) Documentation() (activity string, details strin `This activity contains the following fields: - "team_id": unique ID of the created team. - "team_name": the name of the created team.`, `{ - "team_id": 123, + "team_id": 123, "team_name": "foo" }` } @@ -343,7 +345,7 @@ func (a ActivityTypeDeletedTeam) Documentation() (activity string, details strin `This activity contains the following fields: - "team_id": unique ID of the deleted team. - "team_name": the name of the deleted team.`, `{ - "team_id": 123, + "team_id": 123, "team_name": "foo" }` } @@ -368,7 +370,7 @@ func (a ActivityTypeAppliedSpecTeam) Documentation() (activity string, details s - "name": Name of the team.`, `{ "teams": [ { - "id": 123, + "id": 123, "name": "foo" } ] @@ -391,7 +393,7 @@ func (a ActivityTypeEditedAgentOptions) Documentation() (activity string, detail - "global": "true" if the user updated the global agent options, "false" if the agent options of a team were updated. - "team_id": unique ID of the team for which the agent options were updated (null if global is true). - "team_name": the name of the team for which the agent options were updated (null if global is true).`, `{ - "team_id": 123, + "team_id": 123, "team_name": "foo", "global": false }` @@ -413,7 +415,7 @@ func (a ActivityTypeLiveQuery) Documentation() (activity string, details string, - "targets_count": Number of hosts where the live query was targeted to run. - "query_sql": The SQL query to run on hosts. - "query_name": Name of the query (this field is not set if this was not a saved query).`, `{ - "targets_count": 5000, + "targets_count": 5000, "query_sql": "SELECT * from osquery_info;", "query_name": "foo" }` @@ -442,6 +444,11 @@ type Activity struct { Streamed *bool `json:"-" db:"streamed"` } +// AuthzType implement AuthzTyper to be able to verify access to activities +func (*Activity) AuthzType() string { + return "activity" +} + type ActivityTypeUserLoggedIn struct { PublicIP string `json:"public_ip"` } @@ -677,7 +684,27 @@ func (a ActivityTypeMDMUnenrolled) Documentation() (activity string, details str }` } -// AuthzType implement AuthzTyper to be able to verify access to activities -func (*Activity) AuthzType() string { - return "activity" +type ActivityTypeEditedMacOSMinVersion struct { + TeamID *uint `json:"team_id"` + TeamName *string `json:"team_name"` + MinimumVersion string `json:"minimum_version"` + Deadline string `json:"deadline"` +} + +func (a ActivityTypeEditedMacOSMinVersion) ActivityName() string { + return "edited_macos_min_version" +} + +func (a ActivityTypeEditedMacOSMinVersion) Documentation() (activity string, details string, detailsExample string) { + return `Generated when the minimum required macOS version is modified.`, + `This activity contains the following fields: +- "team_id": The ID of the team that the minimum macOS version applies to, null if it applies to devices that are not in a team. +- "team_name": The name of the team that the minimum macOS version applies to, null if it applies to devices that are not in a team. +- "minimum_version": The minimum macOS version required, empty if the requirement was removed. +- "deadline": The deadline by which the minimum version requirement must be applied, empty if the requirement was removed.`, `{ + "team_id": 3, + "team_name": "Workstations", + "minimum_version": "13.0.1", + "deadline": "2023-06-01" +}` } diff --git a/server/service/appconfig.go b/server/service/appconfig.go index 48eceaad35..08142260c8 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -429,7 +429,22 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle Global: true, }, ); err != nil { - return nil, ctxerr.Wrap(ctx, err, "create activity for app config modification") + return nil, ctxerr.Wrap(ctx, err, "create activity for app config agent options modification") + } + } + + // if the macOS minimum version requirement changed, create the corresponding + // activity + if oldAppConfig.MDM.MacOSUpdates != appConfig.MDM.MacOSUpdates { + if err := svc.ds.NewActivity( + ctx, + authz.UserFromContext(ctx), + fleet.ActivityTypeEditedMacOSMinVersion{ + MinimumVersion: appConfig.MDM.MacOSUpdates.MinimumVersion, + Deadline: appConfig.MDM.MacOSUpdates.Deadline, + }, + ); err != nil { + return nil, ctxerr.Wrap(ctx, err, "create activity for app config macos min version modification") } } diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index d2a55c5a7d..06465e510c 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -115,12 +115,7 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() { require.Equal(t, mdm, team.Config.MDM) // an activity was created for team spec applied - var listActivities listActivitiesResponse - s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &listActivities, "order_key", "id", "order_direction", "desc") - require.True(t, len(listActivities.Activities) > 0) - assert.Equal(t, fleet.ActivityTypeAppliedSpecTeam{}.ActivityName(), listActivities.Activities[0].Type) - require.NotNil(t, listActivities.Activities[0].Details) - assert.JSONEq(t, fmt.Sprintf(`{"teams": [{"id": %d, "name": %q}]}`, team.ID, team.Name), string(*listActivities.Activities[0].Details)) + s.lastActivityMatches(fleet.ActivityTypeAppliedSpecTeam{}.ActivityName(), fmt.Sprintf(`{"teams": [{"id": %d, "name": %q}]}`, team.ID, team.Name), 0) // dry-run with invalid agent options agentOpts = json.RawMessage(`{"config": {"nope": 1}}`) @@ -231,11 +226,7 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() { require.Equal(t, appConfig.Features, team.Config.Features) // an activity was created for the newly created team via the applied spec - s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &listActivities, "order_key", "id", "order_direction", "desc") - require.True(t, len(listActivities.Activities) > 0) - assert.Equal(t, fleet.ActivityTypeAppliedSpecTeam{}.ActivityName(), listActivities.Activities[0].Type) - require.NotNil(t, listActivities.Activities[0].Details) - assert.JSONEq(t, fmt.Sprintf(`{"teams": [{"id": %d, "name": %q}]}`, team.ID, team.Name), string(*listActivities.Activities[0].Details)) + s.lastActivityMatches(fleet.ActivityTypeAppliedSpecTeam{}.ActivityName(), fmt.Sprintf(`{"teams": [{"id": %d, "name": %q}]}`, team.ID, team.Name), 0) // updates teamSpecs = applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{ @@ -680,12 +671,7 @@ func (s *integrationEnterpriseTestSuite) TestTeamEndpoints() { require.Contains(t, string(*tmResp.Team.Config.AgentOptions), `"aws_debug": true`) // left unchanged // list activities, it should have created one for edited_agent_options - var listActivities listActivitiesResponse - s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &listActivities, "order_key", "id", "order_direction", "desc") - require.True(t, len(listActivities.Activities) > 0) - assert.Equal(t, fleet.ActivityTypeEditedAgentOptions{}.ActivityName(), listActivities.Activities[0].Type) - require.NotNil(t, listActivities.Activities[0].Details) - assert.JSONEq(t, fmt.Sprintf(`{"global": false, "team_id": %d, "team_name": %q}`, tm1ID, team.Name), string(*listActivities.Activities[0].Details)) + s.lastActivityMatches(fleet.ActivityTypeEditedAgentOptions{}.ActivityName(), fmt.Sprintf(`{"global": false, "team_id": %d, "team_name": %q}`, tm1ID, team.Name), 0) // modify team agent options - unknown team tmResp.Team = nil @@ -1262,6 +1248,7 @@ func (s *integrationEnterpriseTestSuite) TestMacOSUpdatesConfig() { }, http.StatusOK, &tmResp) require.Equal(t, "10.15.0", tmResp.Team.Config.MDM.MacOSUpdates.MinimumVersion) require.Equal(t, "2021-01-01", tmResp.Team.Config.MDM.MacOSUpdates.Deadline) + s.lastActivityMatches(fleet.ActivityTypeEditedMacOSMinVersion{}.ActivityName(), fmt.Sprintf(`{"team_id": %d, "team_name": %q, "minimum_version": "10.15.0", "deadline": "2021-01-01"}`, team.ID, team.Name), 0) // only update the deadline s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{ @@ -1274,16 +1261,20 @@ func (s *integrationEnterpriseTestSuite) TestMacOSUpdatesConfig() { }, http.StatusOK, &tmResp) require.Equal(t, "10.15.0", tmResp.Team.Config.MDM.MacOSUpdates.MinimumVersion) require.Equal(t, "2025-10-01", tmResp.Team.Config.MDM.MacOSUpdates.Deadline) + lastActivity := s.lastActivityMatches(fleet.ActivityTypeEditedMacOSMinVersion{}.ActivityName(), fmt.Sprintf(`{"team_id": %d, "team_name": %q, "minimum_version": "10.15.0", "deadline": "2025-10-01"}`, team.ID, team.Name), 0) // sending a nil MacOSUpdate config doesn't modify anything s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{MDM: nil}, http.StatusOK, &tmResp) require.Equal(t, "10.15.0", tmResp.Team.Config.MDM.MacOSUpdates.MinimumVersion) require.Equal(t, "2025-10-01", tmResp.Team.Config.MDM.MacOSUpdates.Deadline) + // no new activity is created + s.lastActivityMatches("", "", lastActivity) // sending an empty MacOSUpdate empties both fields s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{MDM: &fleet.TeamMDM{MacOSUpdates: fleet.MacOSUpdates{}}}, http.StatusOK, &tmResp) require.Empty(t, tmResp.Team.Config.MDM.MacOSUpdates.MinimumVersion) require.Empty(t, tmResp.Team.Config.MDM.MacOSUpdates.Deadline) + s.lastActivityMatches(fleet.ActivityTypeEditedMacOSMinVersion{}.ActivityName(), fmt.Sprintf(`{"team_id": %d, "team_name": %q, "minimum_version": "", "deadline": ""}`, team.ID, team.Name), 0) // error checks: @@ -1527,6 +1518,14 @@ func (s *integrationEnterpriseTestSuite) TestDefaultAppleBMTeam() { func (s *integrationEnterpriseTestSuite) TestMDMMacOSUpdates() { t := s.T() + // keep the last activity, to detect newly created ones + var activitiesResp listActivitiesResponse + s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &activitiesResp, "order_key", "a.id", "order_direction", "desc") + var lastActivity uint + if len(activitiesResp.Activities) > 0 { + lastActivity = activitiesResp.Activities[0].ID + } + checkInvalidConfig := func(config string) { // try to set an invalid config acResp := appConfigResponse{} @@ -1536,6 +1535,14 @@ func (s *integrationEnterpriseTestSuite) TestMDMMacOSUpdates() { acResp = appConfigResponse{} s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp) require.Equal(t, fleet.MacOSUpdates{}, acResp.MDM.MacOSUpdates) + + // no activity got created + activitiesResp = listActivitiesResponse{} + s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &activitiesResp, "order_key", "a.id", "order_direction", "desc") + require.Condition(t, func() bool { + return (lastActivity == 0 && len(activitiesResp.Activities) == 0) || + (len(activitiesResp.Activities) > 0 && activitiesResp.Activities[0].ID == lastActivity) + }) } // missing minimum_version @@ -1589,11 +1596,69 @@ func (s *integrationEnterpriseTestSuite) TestMDMMacOSUpdates() { require.Equal(t, "12.3.1", acResp.MDM.MacOSUpdates.MinimumVersion) require.Equal(t, "2022-01-01", acResp.MDM.MacOSUpdates.Deadline) + // edited macos min version activity got created + s.lastActivityMatches(fleet.ActivityTypeEditedMacOSMinVersion{}.ActivityName(), `{"deadline":"2022-01-01", "minimum_version":"12.3.1", "team_id": null, "team_name": null}`, 0) + // get the appconfig acResp = appConfigResponse{} s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp) require.Equal(t, "12.3.1", acResp.MDM.MacOSUpdates.MinimumVersion) require.Equal(t, "2022-01-01", acResp.MDM.MacOSUpdates.Deadline) + + // update the deadline + acResp = appConfigResponse{} + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { + "macos_updates": { + "minimum_version": "12.3.1", + "deadline": "2024-01-01" + } + } + }`), http.StatusOK, &acResp) + require.Equal(t, "12.3.1", acResp.MDM.MacOSUpdates.MinimumVersion) + require.Equal(t, "2024-01-01", acResp.MDM.MacOSUpdates.Deadline) + + // another edited macos min version activity got created + lastActivity = s.lastActivityMatches(fleet.ActivityTypeEditedMacOSMinVersion{}.ActivityName(), `{"deadline":"2024-01-01", "minimum_version":"12.3.1", "team_id": null, "team_name": null}`, 0) + + // update something unrelated - the transparency url + acResp = appConfigResponse{} + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{"fleet_desktop":{"transparency_url": "customURL"}}`), http.StatusOK, &acResp) + + // no activity got created + s.lastActivityMatches("", ``, lastActivity) + + // clear the macos requirement + acResp = appConfigResponse{} + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { + "macos_updates": { + "minimum_version": "", + "deadline": "" + } + } + }`), http.StatusOK, &acResp) + require.Empty(t, acResp.MDM.MacOSUpdates.MinimumVersion) + require.Empty(t, acResp.MDM.MacOSUpdates.Deadline) + + // edited macos min version activity got created with empty requirement + lastActivity = s.lastActivityMatches(fleet.ActivityTypeEditedMacOSMinVersion{}.ActivityName(), `{"deadline":"", "minimum_version":"", "team_id": null, "team_name": null}`, 0) + + // update again with empty macos requirement + acResp = appConfigResponse{} + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { + "macos_updates": { + "minimum_version": "", + "deadline": "" + } + } + }`), http.StatusOK, &acResp) + require.Empty(t, acResp.MDM.MacOSUpdates.MinimumVersion) + require.Empty(t, acResp.MDM.MacOSUpdates.Deadline) + + // no activity got created + s.lastActivityMatches("", ``, lastActivity) } func (s *integrationEnterpriseTestSuite) TestSSOJITProvisioning() { @@ -2192,6 +2257,28 @@ func (s *integrationEnterpriseTestSuite) TestOrbitConfigNudgeSettings() { require.Equal(t, wantCfg.OSVersionRequirements[0].RequiredInstallationDate.String(), "2022-01-04 04:00:00 +0000 UTC") } +// gets the latest activity and checks that it matches any provided properties. +// empty string or 0 id means do not check that property. It returns the ID of that +// latest activity. +func (s *integrationEnterpriseTestSuite) lastActivityMatches(name, details string, id uint) uint { + var listActivities listActivitiesResponse + s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &listActivities, "order_key", "a.id", "order_direction", "desc", "per_page", "1") + require.True(s.T(), len(listActivities.Activities) > 0) + + act := listActivities.Activities[0] + if name != "" { + assert.Equal(s.T(), name, act.Type) + } + if details != "" { + require.NotNil(s.T(), act.Details) + assert.JSONEq(s.T(), details, string(*act.Details)) + } + if id > 0 { + assert.Equal(s.T(), id, act.ID) + } + return act.ID +} + // allEqual compares all fields of a struct. // If a field is a pointer on one side but not on the other, then it follows that pointer. This is useful for optional // arguments.