Calendar config updates -- policy table now has calendar_events_enabled (#17645)

# Checklist for submitter
- [ ] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [x] Added/updated tests
- [x] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Manual QA for all new/changed functionality
This commit is contained in:
Victor Lyuboslavsky 2024-03-14 19:00:51 -05:00 committed by Victor Lyuboslavsky
parent d3e1716572
commit 63e9d49dfc
No known key found for this signature in database
20 changed files with 241 additions and 333 deletions

View File

@ -452,20 +452,6 @@ spec:
) )
// Apply calendar integration // Apply calendar integration
validPolicyID := uint(10)
validPolicyName := "validPolicy"
ds.PoliciesByNameFunc = func(ctx context.Context, names []string, teamID uint) (map[string]*fleet.Policy, error) {
var policies = make(map[string]*fleet.Policy)
for _, name := range names {
if name != validPolicyName {
return nil, &notFoundError{}
}
policies[name] = &fleet.Policy{
PolicyData: fleet.PolicyData{ID: validPolicyID, TeamID: &teamsByName["team1"].ID, Name: validPolicyName},
}
}
return policies, nil
}
filename = writeTmpYml( filename = writeTmpYml(
t, ` t, `
apiVersion: v1 apiVersion: v1
@ -477,8 +463,6 @@ spec:
google_calendar: google_calendar:
email: `+googleCalEmail+` email: `+googleCalEmail+`
enable_calendar_events: true enable_calendar_events: true
policies:
- name: `+validPolicyName+`
webhook_url: https://example.com/webhook webhook_url: https://example.com/webhook
`, `,
) )
@ -488,7 +472,6 @@ spec:
t, fleet.TeamGoogleCalendarIntegration{ t, fleet.TeamGoogleCalendarIntegration{
Email: googleCalEmail, Email: googleCalEmail,
Enable: true, Enable: true,
Policies: []*fleet.PolicyRef{{Name: validPolicyName, ID: validPolicyID}},
WebhookURL: "https://example.com/webhook", WebhookURL: "https://example.com/webhook",
}, *teamsByName["team1"].Config.Integrations.GoogleCalendar, }, *teamsByName["team1"].Config.Integrations.GoogleCalendar,
) )
@ -505,8 +488,6 @@ spec:
google_calendar: google_calendar:
email: not_present_globally@example.com email: not_present_globally@example.com
enable_calendar_events: true enable_calendar_events: true
policies:
- name: `+validPolicyName+`
webhook_url: https://example.com/webhook webhook_url: https://example.com/webhook
`, `,
) )
@ -514,26 +495,6 @@ spec:
_, err = runAppNoChecks([]string{"apply", "-f", filename}) _, err = runAppNoChecks([]string{"apply", "-f", filename})
assert.ErrorContains(t, err, "email must match a global Google Calendar integration email") assert.ErrorContains(t, err, "email must match a global Google Calendar integration email")
// Apply calendar integration -- invalid policy name
filename = writeTmpYml(
t, `
apiVersion: v1
kind: team
spec:
team:
name: team1
integrations:
google_calendar:
email: `+googleCalEmail+`
enable_calendar_events: true
policies:
- name: invalidPolicy
webhook_url: https://example.com/webhook
`,
)
_, err = runAppNoChecks([]string{"apply", "-f", filename})
assert.ErrorContains(t, err, "name is invalid")
// Apply calendar integration -- invalid webhook destination // Apply calendar integration -- invalid webhook destination
filename = writeTmpYml( filename = writeTmpYml(
t, ` t, `
@ -546,8 +507,6 @@ spec:
google_calendar: google_calendar:
email: `+googleCalEmail+` email: `+googleCalEmail+`
enable_calendar_events: true enable_calendar_events: true
policies:
- name: `+validPolicyName+`
webhook_url: bozo webhook_url: bozo
`, `,
) )

View File

@ -340,6 +340,7 @@ func TestGetHosts(t *testing.T) {
AuthorEmail: "alice@example.com", AuthorEmail: "alice@example.com",
Resolution: ptr.String("Some resolution"), Resolution: ptr.String("Some resolution"),
TeamID: ptr.Uint(1), TeamID: ptr.Uint(1),
CalendarEventsEnabled: true,
}, },
Response: "passes", Response: "passes",
}, },
@ -354,6 +355,7 @@ func TestGetHosts(t *testing.T) {
AuthorEmail: "alice@example.com", AuthorEmail: "alice@example.com",
Resolution: nil, Resolution: nil,
TeamID: nil, TeamID: nil,
CalendarEventsEnabled: false,
}, },
Response: "fails", Response: "fails",
}, },

View File

@ -466,12 +466,6 @@ func TestFullTeamGitOps(t *testing.T) {
} }
return nil, nil return nil, nil
} }
ds.PoliciesByNameFunc = func(ctx context.Context, names []string, teamID uint) (map[string]*fleet.Policy, error) {
if slices.Contains(names, "policy1") && slices.Contains(names, "policy2") {
return map[string]*fleet.Policy{"policy1": &policy, "policy2": &policy}, nil
}
return nil, nil
}
ds.DeleteTeamPoliciesFunc = func(ctx context.Context, teamID uint, IDs []uint) ([]uint, error) { ds.DeleteTeamPoliciesFunc = func(ctx context.Context, teamID uint, IDs []uint) ([]uint, error) {
policyDeleted = true policyDeleted = true
assert.Equal(t, []uint{policy.ID}, IDs) assert.Equal(t, []uint{policy.ID}, IDs)
@ -554,7 +548,6 @@ func TestFullTeamGitOps(t *testing.T) {
require.NotNil(t, savedTeam.Config.Integrations.GoogleCalendar) require.NotNil(t, savedTeam.Config.Integrations.GoogleCalendar)
assert.Equal(t, "service@example.com", savedTeam.Config.Integrations.GoogleCalendar.Email) assert.Equal(t, "service@example.com", savedTeam.Config.Integrations.GoogleCalendar.Email)
assert.True(t, savedTeam.Config.Integrations.GoogleCalendar.Enable) assert.True(t, savedTeam.Config.Integrations.GoogleCalendar.Enable)
assert.Len(t, savedTeam.Config.Integrations.GoogleCalendar.Policies, 2)
// Now clear the settings // Now clear the settings
tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml") tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")

View File

@ -76,7 +76,8 @@
"team_id": 1, "team_id": 1,
"updated_at": "0001-01-01T00:00:00Z", "updated_at": "0001-01-01T00:00:00Z",
"created_at": "0001-01-01T00:00:00Z", "created_at": "0001-01-01T00:00:00Z",
"critical": false "critical": false,
"calendar_events_enabled": true
}, },
{ {
"id": 2, "id": 2,
@ -91,7 +92,8 @@
"team_id": null, "team_id": null,
"updated_at": "0001-01-01T00:00:00Z", "updated_at": "0001-01-01T00:00:00Z",
"created_at": "0001-01-01T00:00:00Z", "created_at": "0001-01-01T00:00:00Z",
"critical": false "critical": false,
"calendar_events_enabled": false
} }
], ],
"status": "offline", "status": "offline",

View File

@ -62,6 +62,7 @@ spec:
created_at: "0001-01-01T00:00:00Z" created_at: "0001-01-01T00:00:00Z"
updated_at: "0001-01-01T00:00:00Z" updated_at: "0001-01-01T00:00:00Z"
critical: false critical: false
calendar_events_enabled: true
- author_email: "alice@example.com" - author_email: "alice@example.com"
author_id: 1 author_id: 1
author_name: Alice author_name: Alice
@ -75,6 +76,7 @@ spec:
created_at: "0001-01-01T00:00:00Z" created_at: "0001-01-01T00:00:00Z"
updated_at: "0001-01-01T00:00:00Z" updated_at: "0001-01-01T00:00:00Z"
critical: false critical: false
calendar_events_enabled: false
policy_updated_at: "0001-01-01T00:00:00Z" policy_updated_at: "0001-01-01T00:00:00Z"
public_ip: "" public_ip: ""
primary_ip: "" primary_ip: ""

View File

@ -19,9 +19,6 @@ team_settings:
google_calendar: google_calendar:
email: service@example.com email: service@example.com
enable_calendar_events: true enable_calendar_events: true
policies:
- name: policy1
- name: policy2
webhook_url: https://example.com/google_calendar_webhook webhook_url: https://example.com/google_calendar_webhook
agent_options: agent_options:
command_line_flags: command_line_flags:
@ -97,6 +94,7 @@ policies:
description: This policy should always fail. description: This policy should always fail.
resolution: There is no resolution for this policy. resolution: There is no resolution for this policy.
query: SELECT 1 FROM osquery_info WHERE start_time < 0; query: SELECT 1 FROM osquery_info WHERE start_time < 0;
calendar_events_enabled: true
- name: Passing policy - name: Passing policy
platform: linux,windows,darwin,chrome platform: linux,windows,darwin,chrome
description: This policy should always pass. description: This policy should always pass.

View File

@ -197,6 +197,7 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T
} }
if payload.Integrations != nil { if payload.Integrations != nil {
if payload.Integrations.Jira != nil || payload.Integrations.Zendesk != nil {
// the team integrations must reference an existing global config integration. // the team integrations must reference an existing global config integration.
if _, err := payload.Integrations.MatchWithIntegrations(appCfg.Integrations); err != nil { if _, err := payload.Integrations.MatchWithIntegrations(appCfg.Integrations); err != nil {
return nil, fleet.NewInvalidArgumentError("integrations", err.Error()) return nil, fleet.NewInvalidArgumentError("integrations", err.Error())
@ -209,7 +210,8 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T
team.Config.Integrations.Jira = payload.Integrations.Jira team.Config.Integrations.Jira = payload.Integrations.Jira
team.Config.Integrations.Zendesk = payload.Integrations.Zendesk team.Config.Integrations.Zendesk = payload.Integrations.Zendesk
// Only update the google calendar integration if it's not nil }
// Only update the calendar integration if it's not nil
if payload.Integrations.GoogleCalendar != nil { if payload.Integrations.GoogleCalendar != nil {
invalid := &fleet.InvalidArgumentError{} invalid := &fleet.InvalidArgumentError{}
_ = svc.validateTeamCalendarIntegrations(ctx, team, payload.Integrations.GoogleCalendar, appCfg, invalid) _ = svc.validateTeamCalendarIntegrations(ctx, team, payload.Integrations.GoogleCalendar, appCfg, invalid)
@ -1179,35 +1181,6 @@ func (svc *Service) validateTeamCalendarIntegrations(
} else if u.Scheme != "https" && u.Scheme != "http" { } else if u.Scheme != "https" && u.Scheme != "http" {
invalid.Append("integrations.google_calendar.webhook_url", "webhook_url must be https or http") invalid.Append("integrations.google_calendar.webhook_url", "webhook_url must be https or http")
} }
// Validate policy ids
if len(calendarIntegration.Policies) == 0 {
invalid.Append("integrations.google_calendar.policies", "policies are required")
}
if len(calendarIntegration.Policies) > 0 {
for _, policy := range calendarIntegration.Policies {
policy.Name = strings.TrimSpace(policy.Name)
}
calendarIntegration.Policies = server.RemoveDuplicatesFromSlice(calendarIntegration.Policies)
policyNames := make([]string, 0, len(calendarIntegration.Policies))
for _, policy := range calendarIntegration.Policies {
policyNames = append(policyNames, policy.Name)
}
// Policies must be team policies. Global policies are not allowed.
policyMap, err := svc.ds.PoliciesByName(ctx, policyNames, team.ID)
if err != nil {
level.Error(svc.logger).Log("msg", "error getting policies by name", "names", policyNames, "err", err)
if fleet.IsNotFound(err) {
invalid.Append("integrations.google_calendar.policies[].name", "name is invalid")
} else {
return err
}
} else {
// PoliciesByName guarantees that all policies are present
for _, policy := range calendarIntegration.Policies {
policy.ID = policyMap[policy.Name].ID
}
}
}
return nil return nil
} }

View File

@ -0,0 +1,22 @@
package tables
import (
"database/sql"
"fmt"
)
func init() {
MigrationClient.AddMigration(Up_20240314151747, Down_20240314151747)
}
func Up_20240314151747(tx *sql.Tx) error {
_, err := tx.Exec(`ALTER TABLE policies ADD COLUMN calendar_events_enabled TINYINT(1) UNSIGNED NOT NULL DEFAULT '0'`)
if err != nil {
return fmt.Errorf("failed to add calendar_events_enabled to policies: %w", err)
}
return nil
}
func Down_20240314151747(_ *sql.Tx) error {
return nil
}

View File

@ -0,0 +1,42 @@
package tables
import (
"context"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
)
func TestUp_20240314151747(t *testing.T) {
db := applyUpToPrev(t)
policy1 := execNoErrLastID(
t, db, "INSERT INTO policies (name, query, description, checksum) VALUES (?,?,?,?)", "policy", "", "", "checksum",
)
// Apply current migration.
applyNext(t, db)
var policyCheck []struct {
ID int64 `db:"id"`
CalEnabled bool `db:"calendar_events_enabled"`
}
err := db.SelectContext(context.Background(), &policyCheck, `SELECT id, calendar_events_enabled FROM policies ORDER BY id`)
require.NoError(t, err)
require.Len(t, policyCheck, 1)
assert.Equal(t, policy1, policyCheck[0].ID)
assert.Equal(t, false, policyCheck[0].CalEnabled)
policy2 := execNoErrLastID(
t, db, "INSERT INTO policies (name, query, description, checksum, calendar_events_enabled) VALUES (?,?,?,?,?)", "policy2", "", "",
"checksum2", 1,
)
policyCheck = nil
err = db.SelectContext(context.Background(), &policyCheck, `SELECT id, calendar_events_enabled FROM policies WHERE id = ?`, policy2)
require.NoError(t, err)
require.Len(t, policyCheck, 1)
assert.Equal(t, policy2, policyCheck[0].ID)
assert.Equal(t, true, policyCheck[0].CalEnabled)
}

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"golang.org/x/text/unicode/norm" "golang.org/x/text/unicode/norm"
"sort" "sort"
@ -20,7 +19,7 @@ import (
const policyCols = ` const policyCols = `
p.id, p.team_id, p.resolution, p.name, p.query, p.description, p.id, p.team_id, p.resolution, p.name, p.query, p.description,
p.author_id, p.platforms, p.created_at, p.updated_at, p.critical p.author_id, p.platforms, p.created_at, p.updated_at, p.critical, p.calendar_events_enabled
` `
var policySearchColumns = []string{"p.name"} var policySearchColumns = []string{"p.name"}
@ -116,10 +115,12 @@ func (ds *Datastore) SavePolicy(ctx context.Context, p *fleet.Policy, shouldRemo
p.Name = norm.NFC.String(p.Name) p.Name = norm.NFC.String(p.Name)
sql := ` sql := `
UPDATE policies UPDATE policies
SET name = ?, query = ?, description = ?, resolution = ?, platforms = ?, critical = ?, checksum = ` + policiesChecksumComputedColumn() + ` SET name = ?, query = ?, description = ?, resolution = ?, platforms = ?, critical = ?, calendar_events_enabled = ?, checksum = ` + policiesChecksumComputedColumn() + `
WHERE id = ? WHERE id = ?
` `
result, err := ds.writer(ctx).ExecContext(ctx, sql, p.Name, p.Query, p.Description, p.Resolution, p.Platform, p.Critical, p.ID) result, err := ds.writer(ctx).ExecContext(
ctx, sql, p.Name, p.Query, p.Description, p.Resolution, p.Platform, p.Critical, p.CalendarEventsEnabled, p.ID,
)
if err != nil { if err != nil {
return ctxerr.Wrap(ctx, err, "updating policy") return ctxerr.Wrap(ctx, err, "updating policy")
} }
@ -445,42 +446,6 @@ func (ds *Datastore) PoliciesByID(ctx context.Context, ids []uint) (map[uint]*fl
return policiesByID, nil return policiesByID, nil
} }
func (ds *Datastore) PoliciesByName(ctx context.Context, names []string, teamID uint) (map[string]*fleet.Policy, error) {
sqlQuery := `SELECT ` + policyCols + `
FROM policies p
WHERE p.team_id = ? AND p.name IN (?)`
query, args, err := sqlx.In(sqlQuery, teamID, names)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "building query to get policies by name")
}
var policies []*fleet.Policy
err = sqlx.SelectContext(
ctx,
ds.reader(ctx),
&policies,
query, args...,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ctxerr.Wrap(ctx, notFound("Policy").WithName(fmt.Sprintf("%v", names)))
}
return nil, ctxerr.Wrap(ctx, err, "getting policies by name")
}
policiesByName := make(map[string]*fleet.Policy, len(names))
for _, p := range policies {
policiesByName[p.Name] = p
}
for _, name := range names {
if policiesByName[name] == nil {
return nil, ctxerr.Wrap(ctx, notFound("Policy").WithName(name))
}
}
return policiesByName, nil
}
func (ds *Datastore) DeleteGlobalPolicies(ctx context.Context, ids []uint) ([]uint, error) { func (ds *Datastore) DeleteGlobalPolicies(ctx context.Context, ids []uint) ([]uint, error) {
return deletePolicyDB(ctx, ds.writer(ctx), ids, nil) return deletePolicyDB(ctx, ds.writer(ctx), ids, nil)
} }
@ -562,10 +527,11 @@ func (ds *Datastore) NewTeamPolicy(ctx context.Context, teamID uint, authorID *u
nameUnicode := norm.NFC.String(args.Name) nameUnicode := norm.NFC.String(args.Name)
res, err := ds.writer(ctx).ExecContext(ctx, res, err := ds.writer(ctx).ExecContext(ctx,
fmt.Sprintf( fmt.Sprintf(
`INSERT INTO policies (name, query, description, team_id, resolution, author_id, platforms, critical, checksum) VALUES (?, ?, ?, ?, ?, ?, ?, ?, %s)`, `INSERT INTO policies (name, query, description, team_id, resolution, author_id, platforms, critical, calendar_events_enabled, checksum) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, %s)`,
policiesChecksumComputedColumn(), policiesChecksumComputedColumn(),
), ),
nameUnicode, args.Query, args.Description, teamID, args.Resolution, authorID, args.Platform, args.Critical, nameUnicode, args.Query, args.Description, teamID, args.Resolution, authorID, args.Platform, args.Critical,
args.CalendarEventsEnabled,
) )
switch { switch {
case err == nil: case err == nil:
@ -623,15 +589,17 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs
team_id, team_id,
platforms, platforms,
critical, critical,
calendar_events_enabled,
checksum checksum
) VALUES ( ?, ?, ?, ?, ?, (SELECT IFNULL(MIN(id), NULL) FROM teams WHERE name = ?), ?, ?, %s) ) VALUES ( ?, ?, ?, ?, ?, (SELECT IFNULL(MIN(id), NULL) FROM teams WHERE name = ?), ?, ?, ?, %s)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
query = VALUES(query), query = VALUES(query),
description = VALUES(description), description = VALUES(description),
author_id = VALUES(author_id), author_id = VALUES(author_id),
resolution = VALUES(resolution), resolution = VALUES(resolution),
platforms = VALUES(platforms), platforms = VALUES(platforms),
critical = VALUES(critical) critical = VALUES(critical),
calendar_events_enabled = VALUES(calendar_events_enabled)
`, policiesChecksumComputedColumn(), `, policiesChecksumComputedColumn(),
) )
for _, spec := range specs { for _, spec := range specs {
@ -640,6 +608,7 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs
spec.Name = norm.NFC.String(spec.Name) spec.Name = norm.NFC.String(spec.Name)
res, err := tx.ExecContext(ctx, res, err := tx.ExecContext(ctx,
query, spec.Name, spec.Query, spec.Description, authorID, spec.Resolution, spec.Team, spec.Platform, spec.Critical, query, spec.Name, spec.Query, spec.Description, authorID, spec.Resolution, spec.Team, spec.Platform, spec.Critical,
spec.CalendarEventsEnabled,
) )
if err != nil { if err != nil {
return ctxerr.Wrap(ctx, err, "exec ApplyPolicySpecs insert") return ctxerr.Wrap(ctx, err, "exec ApplyPolicySpecs insert")

View File

@ -38,7 +38,6 @@ func TestPolicies(t *testing.T) {
{"PolicyQueriesForHost", testPolicyQueriesForHost}, {"PolicyQueriesForHost", testPolicyQueriesForHost},
{"PolicyQueriesForHostPlatforms", testPolicyQueriesForHostPlatforms}, {"PolicyQueriesForHostPlatforms", testPolicyQueriesForHostPlatforms},
{"PoliciesByID", testPoliciesByID}, {"PoliciesByID", testPoliciesByID},
{"PoliciesByName", testPoliciesByName},
{"TeamPolicyTransfer", testTeamPolicyTransfer}, {"TeamPolicyTransfer", testTeamPolicyTransfer},
{"ApplyPolicySpec", testApplyPolicySpec}, {"ApplyPolicySpec", testApplyPolicySpec},
{"Save", testPoliciesSave}, {"Save", testPoliciesSave},
@ -587,6 +586,7 @@ func testTeamPolicyProprietary(t *testing.T, ds *Datastore) {
Query: "select 1;", Query: "select 1;",
Description: "query1 desc", Description: "query1 desc",
Resolution: "query1 resolution", Resolution: "query1 resolution",
CalendarEventsEnabled: true,
}) })
require.NoError(t, err) require.NoError(t, err)
@ -616,6 +616,7 @@ func testTeamPolicyProprietary(t *testing.T, ds *Datastore) {
assert.Equal(t, "query1 resolution", *p.Resolution) assert.Equal(t, "query1 resolution", *p.Resolution)
require.NotNil(t, p.AuthorID) require.NotNil(t, p.AuthorID)
assert.Equal(t, user1.ID, *p.AuthorID) assert.Equal(t, user1.ID, *p.AuthorID)
assert.True(t, p.CalendarEventsEnabled)
globalPolicies, err := ds.ListGlobalPolicies(ctx, fleet.ListOptions{}) globalPolicies, err := ds.ListGlobalPolicies(ctx, fleet.ListOptions{})
require.NoError(t, err) require.NoError(t, err)
@ -1115,47 +1116,6 @@ func testPoliciesByID(t *testing.T, ds *Datastore) {
require.ErrorAs(t, err, &nfe) require.ErrorAs(t, err, &nfe)
} }
func testPoliciesByName(t *testing.T, ds *Datastore) {
ctx := context.Background()
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
policyName1 := "policy1"
policyName2 := "policy2"
_ = newTestPolicy(t, ds, user1, policyName1, "darwin", nil)
_ = newTestPolicy(t, ds, user1, policyName2, "darwin", nil)
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: t.Name() + "team1"})
require.NoError(t, err)
// No names provided
_, err = ds.PoliciesByName(context.Background(), []string{}, team1.ID)
require.Error(t, err)
// Policies don't belong to a team
_, err = ds.PoliciesByName(context.Background(), []string{policyName1, policyName2}, team1.ID)
require.Error(t, err)
var nfe fleet.NotFoundError
require.ErrorAs(t, err, &nfe)
policy1 := newTestPolicy(t, ds, user1, policyName1, "darwin", &team1.ID)
policy2 := newTestPolicy(t, ds, user1, policyName2, "darwin", &team1.ID)
policiesByName, err := ds.PoliciesByName(context.Background(), []string{policyName1, policyName2}, team1.ID)
require.NoError(t, err)
require.Len(t, policiesByName, 2)
assert.Equal(t, policiesByName[policyName1].ID, policy1.ID)
assert.Equal(t, policiesByName[policyName2].ID, policy2.ID)
assert.Equal(t, policiesByName[policyName1].Name, policy1.Name)
assert.Equal(t, policiesByName[policyName2].Name, policy2.Name)
// Policy does not exist
_, err = ds.PoliciesByName(context.Background(), []string{"doesn't exist"}, team1.ID)
assert.ErrorAs(t, err, &nfe)
// One exists and one doesn't
_, err = ds.PoliciesByName(context.Background(), []string{policyName1, "doesn't exist"}, team1.ID)
assert.ErrorAs(t, err, &nfe)
}
func testTeamPolicyTransfer(t *testing.T, ds *Datastore) { func testTeamPolicyTransfer(t *testing.T, ds *Datastore) {
ctx := context.Background() ctx := context.Background()
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true) user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
@ -1292,6 +1252,7 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) {
Resolution: "some other resolution", Resolution: "some other resolution",
Team: "team1", Team: "team1",
Platform: "darwin", Platform: "darwin",
CalendarEventsEnabled: true,
}, },
{ {
Name: "query3", Name: "query3",
@ -1326,6 +1287,7 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) {
require.NotNil(t, teamPolicies[0].Resolution) require.NotNil(t, teamPolicies[0].Resolution)
assert.Equal(t, "some other resolution", *teamPolicies[0].Resolution) assert.Equal(t, "some other resolution", *teamPolicies[0].Resolution)
assert.Equal(t, "darwin", teamPolicies[0].Platform) assert.Equal(t, "darwin", teamPolicies[0].Platform)
assert.True(t, teamPolicies[0].CalendarEventsEnabled)
assert.Equal(t, "query3", teamPolicies[1].Name) assert.Equal(t, "query3", teamPolicies[1].Name)
assert.Equal(t, "select 3;", teamPolicies[1].Query) assert.Equal(t, "select 3;", teamPolicies[1].Query)
@ -1335,6 +1297,7 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) {
require.NotNil(t, teamPolicies[1].Resolution) require.NotNil(t, teamPolicies[1].Resolution)
assert.Equal(t, "some other good resolution", *teamPolicies[1].Resolution) assert.Equal(t, "some other good resolution", *teamPolicies[1].Resolution)
assert.Equal(t, "windows,linux", teamPolicies[1].Platform) assert.Equal(t, "windows,linux", teamPolicies[1].Platform)
assert.False(t, teamPolicies[1].CalendarEventsEnabled)
// Make sure apply is idempotent // Make sure apply is idempotent
require.NoError(t, ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{ require.NoError(t, ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{
@ -1353,6 +1316,7 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) {
Resolution: "some other resolution", Resolution: "some other resolution",
Team: "team1", Team: "team1",
Platform: "darwin", Platform: "darwin",
CalendarEventsEnabled: true,
}, },
{ {
Name: "query3", Name: "query3",
@ -1388,6 +1352,7 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) {
Resolution: "some other resolution updated", Resolution: "some other resolution updated",
Team: "team1", // No error, team did not change Team: "team1", // No error, team did not change
Platform: "windows", Platform: "windows",
CalendarEventsEnabled: false,
}, },
})) }))
policies, err = ds.ListGlobalPolicies(ctx, fleet.ListOptions{}) policies, err = ds.ListGlobalPolicies(ctx, fleet.ListOptions{})
@ -1402,6 +1367,7 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) {
require.NotNil(t, policies[0].Resolution) require.NotNil(t, policies[0].Resolution)
assert.Equal(t, "some resolution updated", *policies[0].Resolution) assert.Equal(t, "some resolution updated", *policies[0].Resolution)
assert.Equal(t, "", policies[0].Platform) assert.Equal(t, "", policies[0].Platform)
assert.False(t, policies[0].CalendarEventsEnabled)
teamPolicies, _, err = ds.ListTeamPolicies(ctx, team1.ID, fleet.ListOptions{}, fleet.ListOptions{}) teamPolicies, _, err = ds.ListTeamPolicies(ctx, team1.ID, fleet.ListOptions{}, fleet.ListOptions{})
require.NoError(t, err) require.NoError(t, err)
@ -1486,6 +1452,7 @@ func testPoliciesSave(t *testing.T, ds *Datastore) {
Description: "team1 query desc", Description: "team1 query desc",
Resolution: "team1 query resolution", Resolution: "team1 query resolution",
Critical: true, Critical: true,
CalendarEventsEnabled: true,
} }
tp1, err := ds.NewTeamPolicy(ctx, team1.ID, &user1.ID, payload) tp1, err := ds.NewTeamPolicy(ctx, team1.ID, &user1.ID, payload)
require.NoError(t, err) require.NoError(t, err)
@ -1494,6 +1461,7 @@ func testPoliciesSave(t *testing.T, ds *Datastore) {
require.Equal(t, tp1.Description, payload.Description) require.Equal(t, tp1.Description, payload.Description)
require.Equal(t, *tp1.Resolution, payload.Resolution) require.Equal(t, *tp1.Resolution, payload.Resolution)
require.Equal(t, tp1.Critical, payload.Critical) require.Equal(t, tp1.Critical, payload.Critical)
assert.Equal(t, tp1.CalendarEventsEnabled, payload.CalendarEventsEnabled)
var teamChecksum []uint8 var teamChecksum []uint8
err = ds.writer(context.Background()).Get(&teamChecksum, `SELECT checksum FROM policies WHERE id = ?`, tp1.ID) err = ds.writer(context.Background()).Get(&teamChecksum, `SELECT checksum FROM policies WHERE id = ?`, tp1.ID)
require.NoError(t, err) require.NoError(t, err)
@ -1522,6 +1490,7 @@ func testPoliciesSave(t *testing.T, ds *Datastore) {
tp2.Description = "team1 query desc updated" tp2.Description = "team1 query desc updated"
tp2.Resolution = ptr.String("team1 query resolution updated") tp2.Resolution = ptr.String("team1 query resolution updated")
tp2.Critical = false tp2.Critical = false
tp2.CalendarEventsEnabled = false
err = ds.SavePolicy(ctx, &tp2, true) err = ds.SavePolicy(ctx, &tp2, true)
require.NoError(t, err) require.NoError(t, err)
tp1, err = ds.Policy(ctx, tp1.ID) tp1, err = ds.Policy(ctx, tp1.ID)

View File

@ -588,7 +588,6 @@ type Datastore interface {
ListGlobalPolicies(ctx context.Context, opts ListOptions) ([]*Policy, error) ListGlobalPolicies(ctx context.Context, opts ListOptions) ([]*Policy, error)
PoliciesByID(ctx context.Context, ids []uint) (map[uint]*Policy, error) PoliciesByID(ctx context.Context, ids []uint) (map[uint]*Policy, error)
PoliciesByName(ctx context.Context, names []string, teamID uint) (map[string]*Policy, error)
DeleteGlobalPolicies(ctx context.Context, ids []uint) ([]uint, error) DeleteGlobalPolicies(ctx context.Context, ids []uint) ([]uint, error)
CountPolicies(ctx context.Context, teamID *uint, matchQuery string) (int, error) CountPolicies(ctx context.Context, teamID *uint, matchQuery string) (int, error)
UpdateHostPolicyCounts(ctx context.Context) error UpdateHostPolicyCounts(ctx context.Context) error

View File

@ -114,13 +114,8 @@ func (z TeamZendeskIntegration) UniqueKey() string {
type TeamGoogleCalendarIntegration struct { type TeamGoogleCalendarIntegration struct {
Email string `json:"email"` Email string `json:"email"`
Enable bool `json:"enable_calendar_events"` Enable bool `json:"enable_calendar_events"`
Policies []*PolicyRef `json:"policies"`
WebhookURL string `json:"webhook_url"` WebhookURL string `json:"webhook_url"`
} }
type PolicyRef struct {
Name string `json:"name"`
ID uint `json:"id"`
}
// JiraIntegration configures an instance of an integration with the Jira // JiraIntegration configures an instance of an integration with the Jira
// system. // system.
@ -380,7 +375,7 @@ func ValidateEnabledHostStatusIntegrations(webhook HostStatusWebhookSettings, in
func ValidateGoogleCalendarIntegrations(intgs []*GoogleCalendarIntegration, invalid *InvalidArgumentError) { func ValidateGoogleCalendarIntegrations(intgs []*GoogleCalendarIntegration, invalid *InvalidArgumentError) {
if len(intgs) > 1 { if len(intgs) > 1 {
invalid.Append("integrations.google_calendar", "only one Google Calendar integration is allowed at this time") invalid.Append("integrations.google_calendar", "integrating with >1 Google Workspace service account is not yet supported.")
} }
for _, intg := range intgs { for _, intg := range intgs {
intg.Email = strings.TrimSpace(intg.Email) intg.Email = strings.TrimSpace(intg.Email)

View File

@ -30,6 +30,8 @@ type PolicyPayload struct {
// //
// Empty string targets all platforms. // Empty string targets all platforms.
Platform string Platform string
// CalendarEventsEnabled indicates whether calendar events are enabled for the policy. Only applies to team policies.
CalendarEventsEnabled bool
} }
var ( var (
@ -107,6 +109,8 @@ type ModifyPolicyPayload struct {
Platform *string `json:"platform"` Platform *string `json:"platform"`
// Critical marks the policy as high impact. // Critical marks the policy as high impact.
Critical *bool `json:"critical" premium:"true"` Critical *bool `json:"critical" premium:"true"`
// CalendarEventsEnabled indicates whether calendar events are enabled for the policy. Only applies to team policies.
CalendarEventsEnabled *bool `json:"calendar_events_enabled" premium:"true"`
} }
// Verify verifies the policy payload is valid. // Verify verifies the policy payload is valid.
@ -159,6 +163,8 @@ type PolicyData struct {
// Empty string targets all platforms. // Empty string targets all platforms.
Platform string `json:"platform" db:"platforms"` Platform string `json:"platform" db:"platforms"`
CalendarEventsEnabled bool `json:"calendar_events_enabled" db:"calendar_events_enabled"`
UpdateCreateTimestamps UpdateCreateTimestamps
} }
@ -212,6 +218,8 @@ type PolicySpec struct {
// //
// Empty string targets all platforms. // Empty string targets all platforms.
Platform string `json:"platform,omitempty"` Platform string `json:"platform,omitempty"`
// CalendarEventsEnabled indicates whether calendar events are enabled for the policy. Only applies to team policies.
CalendarEventsEnabled bool `json:"calendar_events_enabled"`
} }
// Verify verifies the policy data is valid. // Verify verifies the policy data is valid.

View File

@ -451,6 +451,11 @@ func TeamSpecFromTeam(t *Team) (*TeamSpec, error) {
webhookSettings.HostStatusWebhook = t.Config.WebhookSettings.HostStatusWebhook webhookSettings.HostStatusWebhook = t.Config.WebhookSettings.HostStatusWebhook
} }
var integrations TeamSpecIntegrations
if t.Config.Integrations.GoogleCalendar != nil {
integrations.GoogleCalendar = t.Config.Integrations.GoogleCalendar
}
return &TeamSpec{ return &TeamSpec{
Name: t.Name, Name: t.Name,
AgentOptions: agentOptions, AgentOptions: agentOptions,
@ -459,5 +464,6 @@ func TeamSpecFromTeam(t *Team) (*TeamSpec, error) {
MDM: mdmSpec, MDM: mdmSpec,
HostExpirySettings: &t.Config.HostExpirySettings, HostExpirySettings: &t.Config.HostExpirySettings,
WebhookSettings: webhookSettings, WebhookSettings: webhookSettings,
Integrations: integrations,
}, nil }, nil
} }

View File

@ -432,8 +432,6 @@ type ListGlobalPoliciesFunc func(ctx context.Context, opts fleet.ListOptions) ([
type PoliciesByIDFunc func(ctx context.Context, ids []uint) (map[uint]*fleet.Policy, error) type PoliciesByIDFunc func(ctx context.Context, ids []uint) (map[uint]*fleet.Policy, error)
type PoliciesByNameFunc func(ctx context.Context, names []string, teamID uint) (map[string]*fleet.Policy, error)
type DeleteGlobalPoliciesFunc func(ctx context.Context, ids []uint) ([]uint, error) type DeleteGlobalPoliciesFunc func(ctx context.Context, ids []uint) ([]uint, error)
type CountPoliciesFunc func(ctx context.Context, teamID *uint, matchQuery string) (int, error) type CountPoliciesFunc func(ctx context.Context, teamID *uint, matchQuery string) (int, error)
@ -1482,9 +1480,6 @@ type DataStore struct {
PoliciesByIDFunc PoliciesByIDFunc PoliciesByIDFunc PoliciesByIDFunc
PoliciesByIDFuncInvoked bool PoliciesByIDFuncInvoked bool
PoliciesByNameFunc PoliciesByNameFunc
PoliciesByNameFuncInvoked bool
DeleteGlobalPoliciesFunc DeleteGlobalPoliciesFunc DeleteGlobalPoliciesFunc DeleteGlobalPoliciesFunc
DeleteGlobalPoliciesFuncInvoked bool DeleteGlobalPoliciesFuncInvoked bool
@ -3576,13 +3571,6 @@ func (s *DataStore) PoliciesByID(ctx context.Context, ids []uint) (map[uint]*fle
return s.PoliciesByIDFunc(ctx, ids) return s.PoliciesByIDFunc(ctx, ids)
} }
func (s *DataStore) PoliciesByName(ctx context.Context, names []string, teamID uint) (map[string]*fleet.Policy, error) {
s.mu.Lock()
s.PoliciesByNameFuncInvoked = true
s.mu.Unlock()
return s.PoliciesByNameFunc(ctx, names, teamID)
}
func (s *DataStore) DeleteGlobalPolicies(ctx context.Context, ids []uint) ([]uint, error) { func (s *DataStore) DeleteGlobalPolicies(ctx context.Context, ids []uint) ([]uint, error) {
s.mu.Lock() s.mu.Lock()
s.DeleteGlobalPoliciesFuncInvoked = true s.DeleteGlobalPoliciesFuncInvoked = true

View File

@ -880,7 +880,6 @@ func (c *Client) DoGitOps(
} }
var mdmAppConfig map[string]interface{} var mdmAppConfig map[string]interface{}
var team map[string]interface{} var team map[string]interface{}
var teamCalendarIntegration map[string]interface{}
if config.TeamName == nil { if config.TeamName == nil {
group.AppConfig = config.OrgSettings group.AppConfig = config.OrgSettings
group.EnrollSecret = &fleet.EnrollSecretSpec{Secrets: config.OrgSettings["secrets"].([]*fleet.EnrollSecret)} group.EnrollSecret = &fleet.EnrollSecretSpec{Secrets: config.OrgSettings["secrets"].([]*fleet.EnrollSecret)}
@ -968,20 +967,14 @@ func (c *Client) DoGitOps(
if !ok { if !ok {
return errors.New("team_settings.integrations config is not a map") return errors.New("team_settings.integrations config is not a map")
} }
if calendar, ok := integrations.(map[string]interface{})["google_calendar"]; ok { if googleCal, ok := integrations.(map[string]interface{})["google_calendar"]; !ok || googleCal == nil {
if calendar == nil { integrations.(map[string]interface{})["google_calendar"] = map[string]interface{}{}
calendar = map[string]interface{}{} } else {
integrations.(map[string]interface{})["google_calendar"] = calendar _, ok = googleCal.(map[string]interface{})
}
teamCalendarIntegration, ok = calendar.(map[string]interface{})
if !ok { if !ok {
return errors.New("team_settings.integrations.google_calendar config is not a map") return errors.New("team_settings.integrations.google_calendar config is not a map")
} }
} }
// We clear the calendar integration and re-apply it after updating policies.
// This is needed because the calendar integration may be referencing policies that need to be
// created/updated.
integrations.(map[string]interface{})["google_calendar"] = map[string]interface{}{}
team["mdm"] = map[string]interface{}{} team["mdm"] = map[string]interface{}{}
mdmAppConfig = team["mdm"].(map[string]interface{}) mdmAppConfig = team["mdm"].(map[string]interface{})
@ -1087,23 +1080,6 @@ func (c *Client) DoGitOps(
return err return err
} }
// Apply calendar integration
if len(teamCalendarIntegration) > 0 {
group = spec.Group{}
team = make(map[string]interface{})
team["name"] = *config.TeamName
team["integrations"] = map[string]interface{}{"google_calendar": teamCalendarIntegration}
rawTeam, err := json.Marshal(team)
if err != nil {
return fmt.Errorf("error marshalling team spec: %w", err)
}
group.Teams = []json.RawMessage{rawTeam}
_, err = c.ApplyGroup(ctx, &group, baseDir, logf, fleet.ApplySpecOptions{DryRun: dryRun})
if err != nil {
return err
}
}
err = c.doGitOpsQueries(config, logFn, dryRun) err = c.doGitOpsQueries(config, logFn, dryRun)
if err != nil { if err != nil {
return err return err

View File

@ -199,11 +199,6 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
"google_calendar": map[string]any{ "google_calendar": map[string]any{
"email": calendarEmail, "email": calendarEmail,
"enable_calendar_events": true, "enable_calendar_events": true,
"policies": []any{
map[string]any{
"name": teamPolicy.Name,
},
},
"webhook_url": calendarWebhookUrl, "webhook_url": calendarWebhookUrl,
}, },
}, },
@ -218,8 +213,6 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
assert.Equal(t, calendarEmail, team.Config.Integrations.GoogleCalendar.Email) assert.Equal(t, calendarEmail, team.Config.Integrations.GoogleCalendar.Email)
assert.Equal(t, calendarWebhookUrl, team.Config.Integrations.GoogleCalendar.WebhookURL) assert.Equal(t, calendarWebhookUrl, team.Config.Integrations.GoogleCalendar.WebhookURL)
assert.True(t, team.Config.Integrations.GoogleCalendar.Enable) assert.True(t, team.Config.Integrations.GoogleCalendar.Enable)
require.Len(t, team.Config.Integrations.GoogleCalendar.Policies, 1)
assert.Equal(t, teamPolicy.ID, team.Config.Integrations.GoogleCalendar.Policies[0].ID)
// dry-run with invalid windows updates // dry-run with invalid windows updates
teamSpecs = map[string]any{ teamSpecs = map[string]any{
@ -3686,7 +3679,7 @@ func (s *integrationEnterpriseTestSuite) TestGlobalPolicyCreateReadPatch() {
} }
func (s *integrationEnterpriseTestSuite) TestTeamPolicyCreateReadPatch() { func (s *integrationEnterpriseTestSuite) TestTeamPolicyCreateReadPatch() {
fields := []string{"Query", "Name", "Description", "Resolution", "Platform", "Critical"} fields := []string{"Query", "Name", "Description", "Resolution", "Platform", "Critical", "CalendarEventsEnabled"}
team1, err := s.ds.NewTeam(context.Background(), &fleet.Team{ team1, err := s.ds.NewTeam(context.Background(), &fleet.Team{
ID: 42, ID: 42,
@ -3703,6 +3696,7 @@ func (s *integrationEnterpriseTestSuite) TestTeamPolicyCreateReadPatch() {
Resolution: "resolution", Resolution: "resolution",
Platform: "linux", Platform: "linux",
Critical: true, Critical: true,
CalendarEventsEnabled: true,
} }
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/policies", team1.ID), createPol1Req, http.StatusOK, &createPol1) s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/policies", team1.ID), createPol1Req, http.StatusOK, &createPol1)
allEqual(s.T(), createPol1Req, createPol1.Policy, fields...) allEqual(s.T(), createPol1Req, createPol1.Policy, fields...)
@ -3715,6 +3709,7 @@ func (s *integrationEnterpriseTestSuite) TestTeamPolicyCreateReadPatch() {
Resolution: "resolution", Resolution: "resolution",
Platform: "linux", Platform: "linux",
Critical: false, Critical: false,
CalendarEventsEnabled: false,
} }
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/policies", team1.ID), createPol2Req, http.StatusOK, &createPol2) s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/policies", team1.ID), createPol2Req, http.StatusOK, &createPol2)
allEqual(s.T(), createPol2Req, createPol2.Policy, fields...) allEqual(s.T(), createPol2Req, createPol2.Policy, fields...)
@ -3736,6 +3731,7 @@ func (s *integrationEnterpriseTestSuite) TestTeamPolicyCreateReadPatch() {
Resolution: ptr.String("newResolution"), Resolution: ptr.String("newResolution"),
Platform: ptr.String("windows"), Platform: ptr.String("windows"),
Critical: ptr.Bool(false), Critical: ptr.Bool(false),
CalendarEventsEnabled: ptr.Bool(false),
}, },
} }
patchPol1 := &modifyTeamPolicyResponse{} patchPol1 := &modifyTeamPolicyResponse{}
@ -3750,6 +3746,7 @@ func (s *integrationEnterpriseTestSuite) TestTeamPolicyCreateReadPatch() {
Resolution: ptr.String("newResolution"), Resolution: ptr.String("newResolution"),
Platform: ptr.String("windows"), Platform: ptr.String("windows"),
Critical: ptr.Bool(true), Critical: ptr.Bool(true),
CalendarEventsEnabled: ptr.Bool(true),
}, },
} }
patchPol2 := &modifyTeamPolicyResponse{} patchPol2 := &modifyTeamPolicyResponse{}

View File

@ -28,6 +28,7 @@ type teamPolicyRequest struct {
Resolution string `json:"resolution"` Resolution string `json:"resolution"`
Platform string `json:"platform"` Platform string `json:"platform"`
Critical bool `json:"critical" premium:"true"` Critical bool `json:"critical" premium:"true"`
CalendarEventsEnabled bool `json:"calendar_events_enabled"`
} }
type teamPolicyResponse struct { type teamPolicyResponse struct {
@ -47,6 +48,7 @@ func teamPolicyEndpoint(ctx context.Context, request interface{}, svc fleet.Serv
Resolution: req.Resolution, Resolution: req.Resolution,
Platform: req.Platform, Platform: req.Platform,
Critical: req.Critical, Critical: req.Critical,
CalendarEventsEnabled: req.CalendarEventsEnabled,
}) })
if err != nil { if err != nil {
return teamPolicyResponse{Err: err}, nil return teamPolicyResponse{Err: err}, nil
@ -390,6 +392,9 @@ func (svc *Service) modifyPolicy(ctx context.Context, teamID *uint, id uint, p f
if p.Critical != nil { if p.Critical != nil {
policy.Critical = *p.Critical policy.Critical = *p.Critical
} }
if p.CalendarEventsEnabled != nil {
policy.CalendarEventsEnabled = *p.CalendarEventsEnabled
}
logging.WithExtras(ctx, "name", policy.Name, "sql", policy.Query) logging.WithExtras(ctx, "name", policy.Name, "sql", policy.Query)
err = svc.ds.SavePolicy(ctx, policy, shouldRemoveAll) err = svc.ds.SavePolicy(ctx, policy, shouldRemoveAll)

View File

@ -124,7 +124,8 @@ func TestTriggerFailingPoliciesWebhookBasic(t *testing.T) {
"passing_host_count": 0, "passing_host_count": 0,
"failing_host_count": 0, "failing_host_count": 0,
"host_count_updated_at": null, "host_count_updated_at": null,
"critical": true "critical": true,
"calendar_events_enabled": false
}, },
"hosts": [ "hosts": [
{ {
@ -193,6 +194,7 @@ func TestTriggerFailingPoliciesWebhookTeam(t *testing.T) {
TeamID: &teamID, TeamID: &teamID,
Resolution: ptr.String("policy1 resolution"), Resolution: ptr.String("policy1 resolution"),
Platform: "darwin", Platform: "darwin",
CalendarEventsEnabled: true,
}, },
}, },
2: { 2: {
@ -309,7 +311,8 @@ func TestTriggerFailingPoliciesWebhookTeam(t *testing.T) {
"passing_host_count": 0, "passing_host_count": 0,
"failing_host_count": 0, "failing_host_count": 0,
"host_count_updated_at": null, "host_count_updated_at": null,
"critical": false "critical": false,
"calendar_events_enabled": true
}, },
"hosts": [ "hosts": [
{ {