diff --git a/cmd/fleet/calendar_cron.go b/cmd/fleet/calendar_cron.go
index e4b1927d55..953747c602 100644
--- a/cmd/fleet/calendar_cron.go
+++ b/cmd/fleet/calendar_cron.go
@@ -110,6 +110,10 @@ func cronCalendarEventsForTeam(
}
if len(policies) == 0 {
+ level.Debug(logger).Log(
+ "msg", "skipping, no calendar policies",
+ "team_id", team.ID,
+ )
return nil
}
@@ -120,6 +124,7 @@ func cronCalendarEventsForTeam(
// - We ignore hosts that are passing all policies and do not have an associated email.
// - We get only one host per email that's failing policies (the one with lower host id).
// - On every host, we get only the first email that matches the domain (sorted lexicographically).
+ // - GetTeamHostsPolicyMemberships returns the hosts that are passing all policies and have a calendar event.
//
policyIDs := make([]uint, 0, len(policies))
@@ -625,7 +630,7 @@ func cronCalendarEventsCleanup(ctx context.Context, ds fleet.Datastore, logger k
}
for _, team := range teams {
- if err := deleteTeamCalendarEvents(ctx, ds, userCalendar, *team); err != nil {
+ if err := cleanupTeamCalendarEvents(ctx, ds, userCalendar, *team); err != nil {
level.Info(logger).Log("msg", "delete team calendar events", "team_id", team.ID, "err", err)
}
}
@@ -666,17 +671,27 @@ func deleteAllCalendarEvents(
return nil
}
-func deleteTeamCalendarEvents(
+func cleanupTeamCalendarEvents(
ctx context.Context,
ds fleet.Datastore,
userCalendar fleet.UserCalendar,
team fleet.Team,
) error {
- if team.Config.Integrations.GoogleCalendar != nil &&
- team.Config.Integrations.GoogleCalendar.Enable {
- // Feature is enabled, nothing to cleanup.
- return nil
+ teamFeatureEnabled := team.Config.Integrations.GoogleCalendar != nil && team.Config.Integrations.GoogleCalendar.Enable
+
+ if teamFeatureEnabled {
+ policies, err := ds.GetCalendarPolicies(ctx, team.ID)
+ if err != nil {
+ return fmt.Errorf("get calendar policy ids: %w", err)
+ }
+ if len(policies) > 0 {
+ // Feature is enabled and there are calendar policies configured, so nothing to do.
+ return nil
+ }
+ // Feature is enabled but there are no calendar policies,
+ // so we want to cleanup all calendar events for the team.
}
+
return deleteAllCalendarEvents(ctx, ds, userCalendar, &team.ID)
}
diff --git a/frontend/pages/admin/IntegrationsPage/cards/Calendars/Calendars.tsx b/frontend/pages/admin/IntegrationsPage/cards/Calendars/Calendars.tsx
index da22ea4801..e9f6ed57c9 100644
--- a/frontend/pages/admin/IntegrationsPage/cards/Calendars/Calendars.tsx
+++ b/frontend/pages/admin/IntegrationsPage/cards/Calendars/Calendars.tsx
@@ -7,6 +7,7 @@ import { AppContext } from "context/app";
import configAPI from "services/entities/config";
// @ts-ignore
import { stringToClipboard } from "utilities/copy_text";
+import paths from "router/paths";
// @ts-ignore
import InputField from "components/forms/fields/InputField";
@@ -17,6 +18,7 @@ import Spinner from "components/Spinner";
import DataError from "components/DataError";
import PremiumFeatureMessage from "components/PremiumFeatureMessage/PremiumFeatureMessage";
import Icon from "components/Icon";
+import Card from "components/Card";
const CREATING_SERVICE_ACCOUNT =
"https://www.fleetdm.com/learn-more-about/creating-service-accounts";
@@ -236,6 +238,9 @@ const Calendars = (): JSX.Element => {
For "Organization" and "Location", select
your calendar's organization.
+
+ Click Create.
+
@@ -277,69 +282,72 @@ const Calendars = (): JSX.Element => {
Click Create to create the key & download a JSON file.
-
- Configure your service account integration in Fleet using the
- form below:
-
+
+
+
+ 5. Configure your service account integration in Fleet using the
+ form below:
+
+ -
+ Paste the full contents of the JSON file downloaded when
+ creating your service account API key.
+
+ -
+ Set your primary domain. (If the end user is signed into
+ multiple Google accounts, this will be used to identify their
+ work calendar.)
+
+ -
+ Save your changes.
+
+
+
- 5. Authorize the service account via domain-wide delegation.
+ 6. Authorize the service account via domain-wide delegation.
-
In Google Workspace, go to{" "}
@@ -381,7 +389,7 @@ const Calendars = (): JSX.Element => {
- 6. Enable the Google Calendar API.
+ 7. Enable the Google Calendar API.
-
In the Google Cloud console API library, go to the Google
@@ -402,8 +410,12 @@ const Calendars = (): JSX.Element => {
- You're ready to automatically schedule calendar events for end
- users.
+ Now head over to{" "}
+ {" "}
+ to finish setup.
>
diff --git a/frontend/pages/admin/IntegrationsPage/cards/Calendars/_styles.scss b/frontend/pages/admin/IntegrationsPage/cards/Calendars/_styles.scss
index 01db771e00..ffd30992ef 100644
--- a/frontend/pages/admin/IntegrationsPage/cards/Calendars/_styles.scss
+++ b/frontend/pages/admin/IntegrationsPage/cards/Calendars/_styles.scss
@@ -16,8 +16,8 @@
margin: $pad-small 0;
}
- form {
- margin-top: $pad-large;
+ .card {
+ margin-top: $pad-small;
}
&__configuration {
diff --git a/schema/osquery_fleet_schema.json b/schema/osquery_fleet_schema.json
index c6f9096d43..adfa7eef1f 100644
--- a/schema/osquery_fleet_schema.json
+++ b/schema/osquery_fleet_schema.json
@@ -3970,7 +3970,7 @@
{
"name": "path",
"description": "Path to extension folder. Defaults to '' on ChromeOS",
- "type": "STRING",
+ "type": "TEXT",
"notes": "",
"hidden": false,
"required": false,
@@ -4067,7 +4067,7 @@
{
"name": "state",
"description": "1 if this extension is enabled",
- "type": "STRING",
+ "type": "TEXT",
"notes": "",
"hidden": false,
"required": false,
@@ -15143,19 +15143,19 @@
"name": "trace_id",
"description": "The ID of a trace event",
"required": false,
- "type": "string"
+ "type": "text"
},
{
"name": "event_type",
"description": "The type of event, this can be logEvent, signpostEvent or stateEvent.",
"required": false,
- "type": "string"
+ "type": "text"
},
{
"name": "format_string",
"description": "The format string used to convert variable content into a string for output.",
"required": false,
- "type": "string"
+ "type": "text"
},
{
"name": "activity_identifier",
@@ -15185,19 +15185,19 @@
"name": "sender_image_uuid",
"description": "The UUID of the library, framework, kernel extension, or mach-o image, that originated the event.",
"required": false,
- "type": "string"
+ "type": "text"
},
{
"name": "sender_image_path",
"description": "The full path of the library, framework, kernel extension, or mach-o image, that originated the event.",
"required": false,
- "type": "string"
+ "type": "text"
},
{
"name": "boot_uuid",
"description": "The boot UUID of the event.",
"required": false,
- "type": "string"
+ "type": "text"
},
{
"name": "process_id",
@@ -15209,7 +15209,7 @@
"name": "process_image_path",
"description": "The full path of the process that originated the event.",
"required": false,
- "type": "string"
+ "type": "text"
},
{
"name": "timestamp",
@@ -15221,7 +15221,7 @@
"name": "event_message",
"description": "The message of the log entry.",
"required": false,
- "type": "string"
+ "type": "text"
},
{
"name": "sender_program_counter",
@@ -25617,7 +25617,7 @@
{
"name": "hostname",
"description": "Network hostname including domain. For ChromeOS, this is only available if the extension was force-installed by an enterprise policy",
- "type": "STRING",
+ "type": "TEXT",
"notes": "",
"hidden": false,
"required": false,
@@ -25635,7 +25635,7 @@
{
"name": "cpu_type",
"description": "CPU type",
- "type": "STRING",
+ "type": "TEXT",
"notes": "",
"hidden": false,
"required": false,
@@ -25658,7 +25658,7 @@
{
"name": "cpu_brand",
"description": "CPU brand string, contains vendor and model",
- "type": "STRING",
+ "type": "TEXT",
"notes": "",
"hidden": false,
"required": false,
@@ -25718,7 +25718,7 @@
{
"name": "physical_memory",
"description": "Total physical memory in bytes",
- "type": "STRING",
+ "type": "TEXT",
"notes": "",
"hidden": false,
"required": false,
@@ -25727,7 +25727,7 @@
{
"name": "hardware_vendor",
"description": "Hardware vendor. For ChromeOS, this is only available if the extension was force-installed by an enterprise policy",
- "type": "STRING",
+ "type": "TEXT",
"notes": "",
"hidden": false,
"required": false,
@@ -25736,7 +25736,7 @@
{
"name": "hardware_model",
"description": "Hardware model. For ChromeOS, this is only available if the extension was force-installed by an enterprise policy",
- "type": "STRING",
+ "type": "TEXT",
"notes": "",
"hidden": false,
"required": false,
@@ -25759,7 +25759,7 @@
{
"name": "hardware_serial",
"description": "The device's serial number. For ChromeOS, this is only available if the extension was force-installed by an enterprise policy",
- "type": "STRING",
+ "type": "TEXT",
"notes": "",
"hidden": false,
"required": false,
@@ -25824,7 +25824,7 @@
{
"name": "computer_name",
"description": "Friendly computer name (optional). For ChromeOS, if the extension wasn't force-installed by an enterprise policy this will default to 'ChromeOS' only",
- "type": "STRING",
+ "type": "TEXT",
"notes": "",
"hidden": false,
"required": false,
@@ -25857,7 +25857,7 @@
"columns": [
{
"name": "idle_state",
- "type": "string",
+ "type": "text",
"description": "Returns \"locked\", \"idle\", or \"active\".",
"required": false
}
@@ -27254,7 +27254,7 @@
{
"name": "email",
"required": false,
- "type": "string",
+ "type": "text",
"description": "Email",
"platforms": [
"chrome"
diff --git a/schema/tables/chrome_extensions.yml b/schema/tables/chrome_extensions.yml
index 932c087acf..a993273c25 100644
--- a/schema/tables/chrome_extensions.yml
+++ b/schema/tables/chrome_extensions.yml
@@ -57,7 +57,7 @@ columns:
- windows
- linux
- name: path
- type: string
+ type: text
description: Path to extension folder. Defaults to '' on ChromeOS
- name: optional_permissions
platforms:
@@ -85,7 +85,7 @@ columns:
- windows
- linux
- name: state
- type: string
+ type: text
- name: install_time
platforms:
- darwin
diff --git a/schema/tables/macadmins_unified_log.yml b/schema/tables/macadmins_unified_log.yml
index f93a7ec614..bd8c6a85af 100644
--- a/schema/tables/macadmins_unified_log.yml
+++ b/schema/tables/macadmins_unified_log.yml
@@ -16,15 +16,15 @@ columns:
- name: trace_id
description: The ID of a trace event
required: false
- type: string
+ type: text
- name: event_type
description: The type of event, this can be logEvent, signpostEvent or stateEvent.
required: false
- type: string
+ type: text
- name: format_string
description: The format string used to convert variable content into a string for output.
required: false
- type: string
+ type: text
- name: activity_identifier
description: The identifier of the log activity.
required: false
@@ -44,15 +44,15 @@ columns:
- name: sender_image_uuid
description: The UUID of the library, framework, kernel extension, or mach-o image, that originated the event.
required: false
- type: string
+ type: text
- name: sender_image_path
description: The full path of the library, framework, kernel extension, or mach-o image, that originated the event.
required: false
- type: string
+ type: text
- name: boot_uuid
description: The boot UUID of the event.
required: false
- type: string
+ type: text
- name: process_id
description: Process ID of the process that generated this log item, which can be joined to multiple other tables including a *PID*.
required: false
@@ -60,7 +60,7 @@ columns:
- name: process_image_path
description: The full path of the process that originated the event.
required: false
- type: string
+ type: text
- name: timestamp
description: Timestamp in [UNIX time format](https://en.wikipedia.org/wiki/Unix_time).
required: false
@@ -68,7 +68,7 @@ columns:
- name: event_message
description: The message of the log entry.
required: false
- type: string
+ type: text
- name: sender_program_counter
description: The program counter of the library, framework, kernel extension, or mach-o image, that originated the event.
required: false
diff --git a/schema/tables/system_info.yml b/schema/tables/system_info.yml
index 4300141df5..38b0b5b2af 100644
--- a/schema/tables/system_info.yml
+++ b/schema/tables/system_info.yml
@@ -56,26 +56,26 @@ columns:
- windows
- linux
- name: hostname
- type: string
+ type: text
description: Network hostname including domain. For ChromeOS, this is only available if the extension was force-installed by an enterprise policy
- name: computer_name
- type: string
+ type: text
description: Friendly computer name (optional). For ChromeOS, if the extension wasn't force-installed by an enterprise policy this will default to 'ChromeOS' only
- name: hardware_serial
- type: string
+ type: text
description: The device's serial number. For ChromeOS, this is only available if the extension was force-installed by an enterprise policy
- name: hardware_vendor
- type: string
+ type: text
description: Hardware vendor. For ChromeOS, this is only available if the extension was force-installed by an enterprise policy
- name: hardware_model
- type: string
+ type: text
description: Hardware model. For ChromeOS, this is only available if the extension was force-installed by an enterprise policy
- name: cpu_brand
- type: string
+ type: text
- name: cpu_type
- type: string
+ type: text
- name: physical_memory
- type: string
+ type: text
examples: >-
See the CPU architecture of a machine as well as who made it and what its
diff --git a/schema/tables/system_state.yml b/schema/tables/system_state.yml
index 323fd7a023..f47031849e 100644
--- a/schema/tables/system_state.yml
+++ b/schema/tables/system_state.yml
@@ -12,7 +12,7 @@ examples: >-
```
columns:
- name: idle_state
- type: string
+ type: text
description: Returns "locked", "idle", or "active".
required: false
evented: false
diff --git a/schema/tables/users.yml b/schema/tables/users.yml
index 574d1d2bf9..ed9469fa7e 100644
--- a/schema/tables/users.yml
+++ b/schema/tables/users.yml
@@ -46,7 +46,7 @@ columns:
- name: uuid
- name: email
required: false
- type: string
+ type: text
description: Email
platforms:
- chrome
diff --git a/server/datastore/mysql/calendar_events.go b/server/datastore/mysql/calendar_events.go
index 45d8d88331..ebd071d81a 100644
--- a/server/datastore/mysql/calendar_events.go
+++ b/server/datastore/mysql/calendar_events.go
@@ -210,7 +210,6 @@ func (ds *Datastore) ListCalendarEvents(ctx context.Context, teamID *uint) ([]*f
var args []interface{}
if teamID != nil {
- // TODO(lucas): Should we add a team_id column to calendar_events?
calendarEventsQuery += ` JOIN host_calendar_events hce ON ce.id=hce.calendar_event_id
JOIN hosts h ON h.id=hce.host_id WHERE h.team_id = ?`
args = append(args, *teamID)
diff --git a/server/datastore/mysql/policies.go b/server/datastore/mysql/policies.go
index 71530961de..098014f699 100644
--- a/server/datastore/mysql/policies.go
+++ b/server/datastore/mysql/policies.go
@@ -1181,28 +1181,30 @@ func (ds *Datastore) GetTeamHostsPolicyMemberships(
query := `
SELECT
COALESCE(sh.email, '') AS email,
- pm.passing AS passing,
+ COALESCE(pm.passing, 1) AS passing,
h.id AS host_id,
- hdn.display_name AS host_display_name,
+ COALESCE(hdn.display_name, '') AS host_display_name,
h.hardware_serial AS host_hardware_serial
- FROM (
+ FROM hosts h
+ LEFT JOIN (
SELECT host_id, BIT_AND(COALESCE(passes, 0)) AS passing
FROM policy_membership
WHERE policy_id IN (?)
GROUP BY host_id
- ) pm
+ ) pm ON h.id = pm.host_id
LEFT JOIN (
SELECT host_id, MIN(email) AS email
FROM host_emails
JOIN hosts ON host_emails.host_id=hosts.id
WHERE email LIKE CONCAT('%@', ?) AND team_id = ?
GROUP BY host_id
- ) sh ON sh.host_id = pm.host_id
- JOIN hosts h ON h.id = pm.host_id
- LEFT JOIN host_display_names hdn ON hdn.host_id = pm.host_id;
+ ) sh ON h.id = sh.host_id
+ LEFT JOIN host_display_names hdn ON h.id = hdn.host_id
+ LEFT JOIN host_calendar_events hce ON h.id = hce.host_id
+ WHERE h.team_id = ? AND ((pm.passing IS NOT NULL AND NOT pm.passing) OR (COALESCE(pm.passing, 1) AND hce.host_id IS NOT NULL));
`
- query, args, err := sqlx.In(query, policyIDs, domain, teamID)
+ query, args, err := sqlx.In(query, policyIDs, domain, teamID, teamID)
if err != nil {
return nil, ctxerr.Wrapf(ctx, err, "build select get team hosts policy memberships query")
}
diff --git a/server/datastore/mysql/policies_test.go b/server/datastore/mysql/policies_test.go
index 15ebeee171..374dbf7281 100644
--- a/server/datastore/mysql/policies_test.go
+++ b/server/datastore/mysql/policies_test.go
@@ -2865,6 +2865,23 @@ func testGetCalendarPolicies(t *testing.T, ds *Datastore) {
func testGetTeamHostsPolicyMemberships(t *testing.T, ds *Datastore) {
ctx := context.Background()
+ //
+ // Test setup:
+ //
+ // team1:
+ // team1Policy1 (calendar), team1Policy2
+ // host1, host5, host6
+ //
+ // team2:
+ // team2Policy1 (calendar), team2Policy2 (calendar)
+ // host2, host3
+ //
+ // global:
+ // Global Policy 1
+ // host4
+ //
+ //
+
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
require.NoError(t, err)
team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team2"})
@@ -2894,11 +2911,16 @@ func testGetTeamHostsPolicyMemberships(t *testing.T, ds *Datastore) {
CalendarEventsEnabled: true,
})
require.NoError(t, err)
+ _, err = ds.NewGlobalPolicy(ctx, nil, fleet.PolicyPayload{
+ Name: "Global Policy 1",
+ Query: "SELECT * FROM foobar;",
+ })
+ require.NoError(t, err)
// Empty teams.
hostsTeam1, err := ds.GetTeamHostsPolicyMemberships(ctx, "example.com", team1.ID, []uint{team1Policy1.ID, team1Policy2.ID})
require.NoError(t, err)
- require.Len(t, hostsTeam1, 0)
+ require.Empty(t, hostsTeam1)
host1, err := ds.NewHost(ctx, &fleet.Host{
OsqueryHostID: ptr.String("host1"),
@@ -2939,11 +2961,35 @@ func testGetTeamHostsPolicyMemberships(t *testing.T, ds *Datastore) {
TeamID: &team1.ID,
})
require.NoError(t, err)
+ host6, err := ds.NewHost(ctx, &fleet.Host{
+ OsqueryHostID: ptr.String("host6"),
+ NodeKey: ptr.String("host6"),
+ HardwareSerial: "serial6",
+ ComputerName: "display_name6",
+ TeamID: &team1.ID,
+ })
+ require.NoError(t, err)
- // No policy results yet.
+ // Some domain that doesn't exist on any of the hosts
+ hostsTeam1, err = ds.GetTeamHostsPolicyMemberships(ctx, "not-exists.com", team1.ID, []uint{team1Policy1.ID, team1Policy2.ID})
+ require.NoError(t, err)
+ require.Empty(t, hostsTeam1)
+
+ // No policy results yet (and no calendar events).
hostsTeam1, err = ds.GetTeamHostsPolicyMemberships(ctx, "example.com", team1.ID, []uint{team1Policy1.ID, team1Policy2.ID})
require.NoError(t, err)
- require.Len(t, hostsTeam1, 0)
+ require.Empty(t, hostsTeam1)
+
+ //
+ // Email setup
+ //
+ // host1 has foo@example.com, zoo@example.com
+ // host2 has foo@example.com, foo@other.com
+ // host3 has zoo@example.com
+ // host4 has foo@example.com
+ // host5 has foo@other.com
+ // host6 has bar@example.com
+ //
err = ds.ReplaceHostDeviceMapping(ctx, host1.ID, []*fleet.HostDeviceMapping{
{HostID: host1.ID, Email: "foo@example.com", Source: "google_chrome_profiles"},
@@ -2973,6 +3019,20 @@ func testGetTeamHostsPolicyMemberships(t *testing.T, ds *Datastore) {
{HostID: host5.ID, Email: "foo@other.com", Source: "google_chrome_profiles"},
}, "google_chrome_profiles")
require.NoError(t, err)
+ err = ds.ReplaceHostDeviceMapping(ctx, host6.ID, []*fleet.HostDeviceMapping{
+ {HostID: host6.ID, Email: "bar@example.com", Source: "google_chrome_profiles"},
+ }, "google_chrome_profiles")
+ require.NoError(t, err)
+
+ //
+ // Results setup
+ //
+ // host1 (team1) is passing team1Policy1 (calendar) and failing team1Policy2.
+ // host2 (team2) is failing team2Policy1 (calendar) and passing team2Policy2 (calendar).
+ // host3 (team2) is passing all policies.
+ // host5 (team1) is failing all policies.
+ // host6 (team1) has not returned results.
+ //
err = ds.RecordPolicyQueryExecutions(ctx, host1, map[uint]*bool{
team1Policy1.ID: ptr.Bool(true),
@@ -3005,9 +3065,34 @@ func testGetTeamHostsPolicyMemberships(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.Len(t, team2Policies, 2)
+ // Only returns the failing host, because the passing hosts do not have a calendar event.
hostsTeam1, err = ds.GetTeamHostsPolicyMemberships(ctx, "example.com", team1.ID, []uint{team1Policies[0].ID})
require.NoError(t, err)
- require.Len(t, hostsTeam1, 2)
+ sort.Slice(hostsTeam1, func(i, j int) bool {
+ return hostsTeam1[i].HostID < hostsTeam1[j].HostID
+ })
+ require.Len(t, hostsTeam1, 1)
+ require.Equal(t, host5.ID, hostsTeam1[0].HostID)
+ require.Empty(t, hostsTeam1[0].Email)
+ require.False(t, hostsTeam1[0].Passing)
+ require.Equal(t, "serial5", hostsTeam1[0].HostHardwareSerial)
+ require.Equal(t, "display_name5", hostsTeam1[0].HostDisplayName)
+
+ //
+ // Create a calendar event on host1 and host6.
+ //
+ now := time.Now()
+ _, err = ds.CreateOrUpdateCalendarEvent(ctx, "foo@example.com", now, now.Add(30*time.Minute), []byte(`{"foo": "bar"}`), host1.ID, fleet.CalendarWebhookStatusPending)
+ require.NoError(t, err)
+ _, err = ds.CreateOrUpdateCalendarEvent(ctx, "bar@example.com", now, now.Add(30*time.Minute), []byte(`{"foo": "bar"}`), host6.ID, fleet.CalendarWebhookStatusPending)
+ require.NoError(t, err)
+
+ hostsTeam1, err = ds.GetTeamHostsPolicyMemberships(ctx, "example.com", team1.ID, []uint{team1Policies[0].ID})
+ require.NoError(t, err)
+ sort.Slice(hostsTeam1, func(i, j int) bool {
+ return hostsTeam1[i].HostID < hostsTeam1[j].HostID
+ })
+ require.Len(t, hostsTeam1, 3)
require.Equal(t, host1.ID, hostsTeam1[0].HostID)
require.Equal(t, "foo@example.com", hostsTeam1[0].Email)
require.True(t, hostsTeam1[0].Passing)
@@ -3018,6 +3103,15 @@ func testGetTeamHostsPolicyMemberships(t *testing.T, ds *Datastore) {
require.False(t, hostsTeam1[1].Passing)
require.Equal(t, "serial5", hostsTeam1[1].HostHardwareSerial)
require.Equal(t, "display_name5", hostsTeam1[1].HostDisplayName)
+ require.Equal(t, host6.ID, hostsTeam1[2].HostID)
+ require.Equal(t, "bar@example.com", hostsTeam1[2].Email)
+ require.True(t, hostsTeam1[2].Passing)
+ require.Equal(t, "serial6", hostsTeam1[2].HostHardwareSerial)
+ require.Equal(t, "display_name6", hostsTeam1[2].HostDisplayName)
+
+ //
+ // Move host 4 to team1 and have it fail all team1 policies.
+ //
err = ds.AddHostsToTeam(ctx, &team1.ID, []uint{host4.ID})
require.NoError(t, err)
@@ -3029,7 +3123,10 @@ func testGetTeamHostsPolicyMemberships(t *testing.T, ds *Datastore) {
hostsTeam1, err = ds.GetTeamHostsPolicyMemberships(ctx, "example.com", team1.ID, []uint{team1Policies[0].ID})
require.NoError(t, err)
- require.Len(t, hostsTeam1, 3)
+ require.Len(t, hostsTeam1, 4)
+ sort.Slice(hostsTeam1, func(i, j int) bool {
+ return hostsTeam1[i].HostID < hostsTeam1[j].HostID
+ })
require.Equal(t, host1.ID, hostsTeam1[0].HostID)
require.Equal(t, "foo@example.com", hostsTeam1[0].Email)
require.True(t, hostsTeam1[0].Passing)
@@ -3045,10 +3142,44 @@ func testGetTeamHostsPolicyMemberships(t *testing.T, ds *Datastore) {
require.False(t, hostsTeam1[2].Passing)
require.Equal(t, "serial5", hostsTeam1[2].HostHardwareSerial)
require.Equal(t, "display_name5", hostsTeam1[2].HostDisplayName)
+ require.Equal(t, host6.ID, hostsTeam1[3].HostID)
+ require.Equal(t, "bar@example.com", hostsTeam1[3].Email)
+ require.True(t, hostsTeam1[3].Passing)
+ require.Equal(t, "serial6", hostsTeam1[3].HostHardwareSerial)
+ require.Equal(t, "display_name6", hostsTeam1[3].HostDisplayName)
+
+ //
+ // host3 doesn't have a calendar event so it's not returned by GetTeamHostsPolicyMemberships.
+ //
hostsTeam2, err := ds.GetTeamHostsPolicyMemberships(ctx, "example.com", team2.ID, []uint{team2Policies[0].ID, team2Policies[1].ID})
require.NoError(t, err)
+ require.Len(t, hostsTeam2, 1)
+ require.Equal(t, host2.ID, hostsTeam2[0].HostID)
+ require.Equal(t, "foo@example.com", hostsTeam2[0].Email)
+ require.False(t, hostsTeam2[0].Passing)
+ require.Equal(t, "serial2", hostsTeam2[0].HostHardwareSerial)
+ require.Equal(t, "display_name2", hostsTeam2[0].HostDisplayName)
+
+ //
+ // Create a calendar event on host2 and host3.
+ //
+ now = time.Now()
+ _, err = ds.CreateOrUpdateCalendarEvent(ctx, "foo@example.com", now, now.Add(30*time.Minute), []byte(`{"foo": "bar"}`), host2.ID, fleet.CalendarWebhookStatusPending)
+ require.NoError(t, err)
+ calendarEventHost3, err := ds.CreateOrUpdateCalendarEvent(ctx, "zoo@example.com", now, now.Add(30*time.Minute), []byte(`{"foo": "bar"}`), host3.ID, fleet.CalendarWebhookStatusPending)
+ require.NoError(t, err)
+
+ //
+ // Now it should return host3 because it's passing and has a calendar event.
+ //
+
+ hostsTeam2, err = ds.GetTeamHostsPolicyMemberships(ctx, "example.com", team2.ID, []uint{team2Policies[0].ID, team2Policies[1].ID})
+ require.NoError(t, err)
require.Len(t, hostsTeam2, 2)
+ sort.Slice(hostsTeam2, func(i, j int) bool {
+ return hostsTeam2[i].HostID < hostsTeam1[j].HostID
+ })
require.Equal(t, host2.ID, hostsTeam2[0].HostID)
require.Equal(t, "foo@example.com", hostsTeam2[0].Email)
require.False(t, hostsTeam2[0].Passing)
@@ -3059,4 +3190,71 @@ func testGetTeamHostsPolicyMemberships(t *testing.T, ds *Datastore) {
require.True(t, hostsTeam2[1].Passing)
require.Equal(t, "serial3", hostsTeam2[1].HostHardwareSerial)
require.Equal(t, "display_name3", hostsTeam2[1].HostDisplayName)
+
+ //
+ // Make host2 pass all policies.
+ //
+
+ err = ds.RecordPolicyQueryExecutions(ctx, host2, map[uint]*bool{
+ team2Policy1.ID: ptr.Bool(true),
+ team2Policy2.ID: ptr.Bool(true),
+ }, time.Now(), false)
+ require.NoError(t, err)
+
+ hostsTeam2, err = ds.GetTeamHostsPolicyMemberships(ctx, "example.com", team2.ID, []uint{team2Policies[0].ID, team2Policies[1].ID})
+ require.NoError(t, err)
+ require.Len(t, hostsTeam2, 2)
+ sort.Slice(hostsTeam2, func(i, j int) bool {
+ return hostsTeam2[i].HostID < hostsTeam1[j].HostID
+ })
+ require.Equal(t, host2.ID, hostsTeam2[0].HostID)
+ require.Equal(t, "foo@example.com", hostsTeam2[0].Email)
+ require.True(t, hostsTeam2[0].Passing)
+ require.Equal(t, "serial2", hostsTeam2[0].HostHardwareSerial)
+ require.Equal(t, "display_name2", hostsTeam2[0].HostDisplayName)
+ require.Equal(t, host3.ID, hostsTeam2[1].HostID)
+ require.Equal(t, "zoo@example.com", hostsTeam2[1].Email)
+ require.True(t, hostsTeam2[1].Passing)
+ require.Equal(t, "serial3", hostsTeam2[1].HostHardwareSerial)
+ require.Equal(t, "display_name3", hostsTeam2[1].HostDisplayName)
+
+ //
+ // Delete host3 calendar event
+ //
+
+ err = ds.DeleteCalendarEvent(ctx, calendarEventHost3.ID)
+ require.NoError(t, err)
+
+ hostsTeam2, err = ds.GetTeamHostsPolicyMemberships(ctx, "example.com", team2.ID, []uint{team2Policies[0].ID, team2Policies[1].ID})
+ require.NoError(t, err)
+ require.Len(t, hostsTeam2, 1)
+ require.Equal(t, host2.ID, hostsTeam2[0].HostID)
+ require.Equal(t, "foo@example.com", hostsTeam2[0].Email)
+ require.True(t, hostsTeam2[0].Passing)
+ require.Equal(t, "serial2", hostsTeam2[0].HostHardwareSerial)
+ require.Equal(t, "display_name2", hostsTeam2[0].HostDisplayName)
+
+ //
+ // Edit team2Policy1 platform (which removes all its policy_membership entries).
+ //
+
+ team2Policy1.Platform = "darwin"
+ err = ds.SavePolicy(ctx, team1Policy1, false)
+ require.NoError(t, err)
+ team1Policy1.Platform = "darwin"
+ err = ds.SavePolicy(ctx, team2Policy1, false)
+ require.NoError(t, err)
+
+ //
+ // We should still get host2 as passing because it has an associated calendar event.
+ //
+
+ hostsTeam2, err = ds.GetTeamHostsPolicyMemberships(ctx, "example.com", team2.ID, []uint{team2Policies[0].ID, team2Policies[1].ID})
+ require.NoError(t, err)
+ require.Len(t, hostsTeam2, 1)
+ require.Equal(t, host2.ID, hostsTeam2[0].HostID)
+ require.Equal(t, "foo@example.com", hostsTeam2[0].Email)
+ require.True(t, hostsTeam2[0].Passing)
+ require.Equal(t, "serial2", hostsTeam2[0].HostHardwareSerial)
+ require.Equal(t, "display_name2", hostsTeam2[0].HostDisplayName)
}