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
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(
t, `
apiVersion: v1
@ -477,8 +463,6 @@ spec:
google_calendar:
email: `+googleCalEmail+`
enable_calendar_events: true
policies:
- name: `+validPolicyName+`
webhook_url: https://example.com/webhook
`,
)
@ -488,7 +472,6 @@ spec:
t, fleet.TeamGoogleCalendarIntegration{
Email: googleCalEmail,
Enable: true,
Policies: []*fleet.PolicyRef{{Name: validPolicyName, ID: validPolicyID}},
WebhookURL: "https://example.com/webhook",
}, *teamsByName["team1"].Config.Integrations.GoogleCalendar,
)
@ -505,8 +488,6 @@ spec:
google_calendar:
email: not_present_globally@example.com
enable_calendar_events: true
policies:
- name: `+validPolicyName+`
webhook_url: https://example.com/webhook
`,
)
@ -514,26 +495,6 @@ spec:
_, err = runAppNoChecks([]string{"apply", "-f", filename})
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
filename = writeTmpYml(
t, `
@ -546,8 +507,6 @@ spec:
google_calendar:
email: `+googleCalEmail+`
enable_calendar_events: true
policies:
- name: `+validPolicyName+`
webhook_url: bozo
`,
)

View File

@ -331,29 +331,31 @@ func TestGetHosts(t *testing.T) {
return []*fleet.HostPolicy{
{
PolicyData: fleet.PolicyData{
ID: 1,
Name: "query1",
Query: defaultPolicyQuery,
Description: "Some description",
AuthorID: ptr.Uint(1),
AuthorName: "Alice",
AuthorEmail: "alice@example.com",
Resolution: ptr.String("Some resolution"),
TeamID: ptr.Uint(1),
ID: 1,
Name: "query1",
Query: defaultPolicyQuery,
Description: "Some description",
AuthorID: ptr.Uint(1),
AuthorName: "Alice",
AuthorEmail: "alice@example.com",
Resolution: ptr.String("Some resolution"),
TeamID: ptr.Uint(1),
CalendarEventsEnabled: true,
},
Response: "passes",
},
{
PolicyData: fleet.PolicyData{
ID: 2,
Name: "query2",
Query: defaultPolicyQuery,
Description: "",
AuthorID: ptr.Uint(1),
AuthorName: "Alice",
AuthorEmail: "alice@example.com",
Resolution: nil,
TeamID: nil,
ID: 2,
Name: "query2",
Query: defaultPolicyQuery,
Description: "",
AuthorID: ptr.Uint(1),
AuthorName: "Alice",
AuthorEmail: "alice@example.com",
Resolution: nil,
TeamID: nil,
CalendarEventsEnabled: false,
},
Response: "fails",
},

View File

@ -466,12 +466,6 @@ func TestFullTeamGitOps(t *testing.T) {
}
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) {
policyDeleted = true
assert.Equal(t, []uint{policy.ID}, IDs)
@ -554,7 +548,6 @@ func TestFullTeamGitOps(t *testing.T) {
require.NotNil(t, savedTeam.Config.Integrations.GoogleCalendar)
assert.Equal(t, "service@example.com", savedTeam.Config.Integrations.GoogleCalendar.Email)
assert.True(t, savedTeam.Config.Integrations.GoogleCalendar.Enable)
assert.Len(t, savedTeam.Config.Integrations.GoogleCalendar.Policies, 2)
// Now clear the settings
tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")

View File

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

View File

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

View File

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

View File

@ -197,19 +197,21 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T
}
if payload.Integrations != nil {
// the team integrations must reference an existing global config integration.
if _, err := payload.Integrations.MatchWithIntegrations(appCfg.Integrations); err != nil {
return nil, fleet.NewInvalidArgumentError("integrations", err.Error())
}
if payload.Integrations.Jira != nil || payload.Integrations.Zendesk != nil {
// the team integrations must reference an existing global config integration.
if _, err := payload.Integrations.MatchWithIntegrations(appCfg.Integrations); err != nil {
return nil, fleet.NewInvalidArgumentError("integrations", err.Error())
}
// integrations must be unique
if err := payload.Integrations.Validate(); err != nil {
return nil, fleet.NewInvalidArgumentError("integrations", err.Error())
}
// integrations must be unique
if err := payload.Integrations.Validate(); err != nil {
return nil, fleet.NewInvalidArgumentError("integrations", err.Error())
}
team.Config.Integrations.Jira = payload.Integrations.Jira
team.Config.Integrations.Zendesk = payload.Integrations.Zendesk
// Only update the google calendar integration if it's not nil
team.Config.Integrations.Jira = payload.Integrations.Jira
team.Config.Integrations.Zendesk = payload.Integrations.Zendesk
}
// Only update the calendar integration if it's not nil
if payload.Integrations.GoogleCalendar != nil {
invalid := &fleet.InvalidArgumentError{}
_ = 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" {
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
}

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"
"database/sql"
"encoding/json"
"errors"
"fmt"
"golang.org/x/text/unicode/norm"
"sort"
@ -20,7 +19,7 @@ import (
const policyCols = `
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"}
@ -116,10 +115,12 @@ func (ds *Datastore) SavePolicy(ctx context.Context, p *fleet.Policy, shouldRemo
p.Name = norm.NFC.String(p.Name)
sql := `
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 = ?
`
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 {
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
}
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) {
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)
res, err := ds.writer(ctx).ExecContext(ctx,
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(),
),
nameUnicode, args.Query, args.Description, teamID, args.Resolution, authorID, args.Platform, args.Critical,
args.CalendarEventsEnabled,
)
switch {
case err == nil:
@ -623,15 +589,17 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs
team_id,
platforms,
critical,
calendar_events_enabled,
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
query = VALUES(query),
description = VALUES(description),
author_id = VALUES(author_id),
resolution = VALUES(resolution),
platforms = VALUES(platforms),
critical = VALUES(critical)
critical = VALUES(critical),
calendar_events_enabled = VALUES(calendar_events_enabled)
`, policiesChecksumComputedColumn(),
)
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)
res, err := tx.ExecContext(ctx,
query, spec.Name, spec.Query, spec.Description, authorID, spec.Resolution, spec.Team, spec.Platform, spec.Critical,
spec.CalendarEventsEnabled,
)
if err != nil {
return ctxerr.Wrap(ctx, err, "exec ApplyPolicySpecs insert")

View File

@ -38,7 +38,6 @@ func TestPolicies(t *testing.T) {
{"PolicyQueriesForHost", testPolicyQueriesForHost},
{"PolicyQueriesForHostPlatforms", testPolicyQueriesForHostPlatforms},
{"PoliciesByID", testPoliciesByID},
{"PoliciesByName", testPoliciesByName},
{"TeamPolicyTransfer", testTeamPolicyTransfer},
{"ApplyPolicySpec", testApplyPolicySpec},
{"Save", testPoliciesSave},
@ -583,10 +582,11 @@ func testTeamPolicyProprietary(t *testing.T, ds *Datastore) {
require.Error(t, err)
p, err := ds.NewTeamPolicy(ctx, team1.ID, &user1.ID, fleet.PolicyPayload{
Name: "query1",
Query: "select 1;",
Description: "query1 desc",
Resolution: "query1 resolution",
Name: "query1",
Query: "select 1;",
Description: "query1 desc",
Resolution: "query1 resolution",
CalendarEventsEnabled: true,
})
require.NoError(t, err)
@ -616,6 +616,7 @@ func testTeamPolicyProprietary(t *testing.T, ds *Datastore) {
assert.Equal(t, "query1 resolution", *p.Resolution)
require.NotNil(t, p.AuthorID)
assert.Equal(t, user1.ID, *p.AuthorID)
assert.True(t, p.CalendarEventsEnabled)
globalPolicies, err := ds.ListGlobalPolicies(ctx, fleet.ListOptions{})
require.NoError(t, err)
@ -1115,47 +1116,6 @@ func testPoliciesByID(t *testing.T, ds *Datastore) {
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) {
ctx := context.Background()
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
@ -1286,12 +1246,13 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) {
Platform: "",
},
{
Name: "query2",
Query: "select 2;",
Description: "query2 desc",
Resolution: "some other resolution",
Team: "team1",
Platform: "darwin",
Name: "query2",
Query: "select 2;",
Description: "query2 desc",
Resolution: "some other resolution",
Team: "team1",
Platform: "darwin",
CalendarEventsEnabled: true,
},
{
Name: "query3",
@ -1326,6 +1287,7 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) {
require.NotNil(t, teamPolicies[0].Resolution)
assert.Equal(t, "some other resolution", *teamPolicies[0].Resolution)
assert.Equal(t, "darwin", teamPolicies[0].Platform)
assert.True(t, teamPolicies[0].CalendarEventsEnabled)
assert.Equal(t, "query3", teamPolicies[1].Name)
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)
assert.Equal(t, "some other good resolution", *teamPolicies[1].Resolution)
assert.Equal(t, "windows,linux", teamPolicies[1].Platform)
assert.False(t, teamPolicies[1].CalendarEventsEnabled)
// Make sure apply is idempotent
require.NoError(t, ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{
@ -1347,12 +1310,13 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) {
Platform: "",
},
{
Name: "query2",
Query: "select 2;",
Description: "query2 desc",
Resolution: "some other resolution",
Team: "team1",
Platform: "darwin",
Name: "query2",
Query: "select 2;",
Description: "query2 desc",
Resolution: "some other resolution",
Team: "team1",
Platform: "darwin",
CalendarEventsEnabled: true,
},
{
Name: "query3",
@ -1382,12 +1346,13 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) {
Platform: "",
},
{
Name: "query2",
Query: "select 2 from updated;",
Description: "query2 desc updated",
Resolution: "some other resolution updated",
Team: "team1", // No error, team did not change
Platform: "windows",
Name: "query2",
Query: "select 2 from updated;",
Description: "query2 desc updated",
Resolution: "some other resolution updated",
Team: "team1", // No error, team did not change
Platform: "windows",
CalendarEventsEnabled: false,
},
}))
policies, err = ds.ListGlobalPolicies(ctx, fleet.ListOptions{})
@ -1402,6 +1367,7 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) {
require.NotNil(t, policies[0].Resolution)
assert.Equal(t, "some resolution updated", *policies[0].Resolution)
assert.Equal(t, "", policies[0].Platform)
assert.False(t, policies[0].CalendarEventsEnabled)
teamPolicies, _, err = ds.ListTeamPolicies(ctx, team1.ID, fleet.ListOptions{}, fleet.ListOptions{})
require.NoError(t, err)
@ -1481,11 +1447,12 @@ func testPoliciesSave(t *testing.T, ds *Datastore) {
assert.Equal(t, computeChecksum(*gp), hex.EncodeToString(globalChecksum))
payload = fleet.PolicyPayload{
Name: "team1 query",
Query: "select 2;",
Description: "team1 query desc",
Resolution: "team1 query resolution",
Critical: true,
Name: "team1 query",
Query: "select 2;",
Description: "team1 query desc",
Resolution: "team1 query resolution",
Critical: true,
CalendarEventsEnabled: true,
}
tp1, err := ds.NewTeamPolicy(ctx, team1.ID, &user1.ID, payload)
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.Resolution, payload.Resolution)
require.Equal(t, tp1.Critical, payload.Critical)
assert.Equal(t, tp1.CalendarEventsEnabled, payload.CalendarEventsEnabled)
var teamChecksum []uint8
err = ds.writer(context.Background()).Get(&teamChecksum, `SELECT checksum FROM policies WHERE id = ?`, tp1.ID)
require.NoError(t, err)
@ -1522,6 +1490,7 @@ func testPoliciesSave(t *testing.T, ds *Datastore) {
tp2.Description = "team1 query desc updated"
tp2.Resolution = ptr.String("team1 query resolution updated")
tp2.Critical = false
tp2.CalendarEventsEnabled = false
err = ds.SavePolicy(ctx, &tp2, true)
require.NoError(t, err)
tp1, err = ds.Policy(ctx, tp1.ID)

View File

@ -588,7 +588,6 @@ type Datastore interface {
ListGlobalPolicies(ctx context.Context, opts ListOptions) ([]*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)
CountPolicies(ctx context.Context, teamID *uint, matchQuery string) (int, error)
UpdateHostPolicyCounts(ctx context.Context) error

View File

@ -112,14 +112,9 @@ func (z TeamZendeskIntegration) UniqueKey() string {
}
type TeamGoogleCalendarIntegration struct {
Email string `json:"email"`
Enable bool `json:"enable_calendar_events"`
Policies []*PolicyRef `json:"policies"`
WebhookURL string `json:"webhook_url"`
}
type PolicyRef struct {
Name string `json:"name"`
ID uint `json:"id"`
Email string `json:"email"`
Enable bool `json:"enable_calendar_events"`
WebhookURL string `json:"webhook_url"`
}
// JiraIntegration configures an instance of an integration with the Jira
@ -380,7 +375,7 @@ func ValidateEnabledHostStatusIntegrations(webhook HostStatusWebhookSettings, in
func ValidateGoogleCalendarIntegrations(intgs []*GoogleCalendarIntegration, invalid *InvalidArgumentError) {
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 {
intg.Email = strings.TrimSpace(intg.Email)

View File

@ -30,6 +30,8 @@ type PolicyPayload struct {
//
// Empty string targets all platforms.
Platform string
// CalendarEventsEnabled indicates whether calendar events are enabled for the policy. Only applies to team policies.
CalendarEventsEnabled bool
}
var (
@ -107,6 +109,8 @@ type ModifyPolicyPayload struct {
Platform *string `json:"platform"`
// Critical marks the policy as high impact.
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.
@ -159,6 +163,8 @@ type PolicyData struct {
// Empty string targets all platforms.
Platform string `json:"platform" db:"platforms"`
CalendarEventsEnabled bool `json:"calendar_events_enabled" db:"calendar_events_enabled"`
UpdateCreateTimestamps
}
@ -212,6 +218,8 @@ type PolicySpec struct {
//
// Empty string targets all platforms.
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.

View File

@ -451,6 +451,11 @@ func TeamSpecFromTeam(t *Team) (*TeamSpec, error) {
webhookSettings.HostStatusWebhook = t.Config.WebhookSettings.HostStatusWebhook
}
var integrations TeamSpecIntegrations
if t.Config.Integrations.GoogleCalendar != nil {
integrations.GoogleCalendar = t.Config.Integrations.GoogleCalendar
}
return &TeamSpec{
Name: t.Name,
AgentOptions: agentOptions,
@ -459,5 +464,6 @@ func TeamSpecFromTeam(t *Team) (*TeamSpec, error) {
MDM: mdmSpec,
HostExpirySettings: &t.Config.HostExpirySettings,
WebhookSettings: webhookSettings,
Integrations: integrations,
}, 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 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 CountPoliciesFunc func(ctx context.Context, teamID *uint, matchQuery string) (int, error)
@ -1482,9 +1480,6 @@ type DataStore struct {
PoliciesByIDFunc PoliciesByIDFunc
PoliciesByIDFuncInvoked bool
PoliciesByNameFunc PoliciesByNameFunc
PoliciesByNameFuncInvoked bool
DeleteGlobalPoliciesFunc DeleteGlobalPoliciesFunc
DeleteGlobalPoliciesFuncInvoked bool
@ -3576,13 +3571,6 @@ func (s *DataStore) PoliciesByID(ctx context.Context, ids []uint) (map[uint]*fle
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) {
s.mu.Lock()
s.DeleteGlobalPoliciesFuncInvoked = true

View File

@ -880,7 +880,6 @@ func (c *Client) DoGitOps(
}
var mdmAppConfig map[string]interface{}
var team map[string]interface{}
var teamCalendarIntegration map[string]interface{}
if config.TeamName == nil {
group.AppConfig = config.OrgSettings
group.EnrollSecret = &fleet.EnrollSecretSpec{Secrets: config.OrgSettings["secrets"].([]*fleet.EnrollSecret)}
@ -968,20 +967,14 @@ func (c *Client) DoGitOps(
if !ok {
return errors.New("team_settings.integrations config is not a map")
}
if calendar, ok := integrations.(map[string]interface{})["google_calendar"]; ok {
if calendar == nil {
calendar = map[string]interface{}{}
integrations.(map[string]interface{})["google_calendar"] = calendar
}
teamCalendarIntegration, ok = calendar.(map[string]interface{})
if googleCal, ok := integrations.(map[string]interface{})["google_calendar"]; !ok || googleCal == nil {
integrations.(map[string]interface{})["google_calendar"] = map[string]interface{}{}
} else {
_, ok = googleCal.(map[string]interface{})
if !ok {
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{}{}
mdmAppConfig = team["mdm"].(map[string]interface{})
@ -1087,23 +1080,6 @@ func (c *Client) DoGitOps(
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)
if err != nil {
return err

View File

@ -199,12 +199,7 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
"google_calendar": map[string]any{
"email": calendarEmail,
"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, calendarWebhookUrl, team.Config.Integrations.GoogleCalendar.WebhookURL)
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
teamSpecs = map[string]any{
@ -3686,7 +3679,7 @@ func (s *integrationEnterpriseTestSuite) TestGlobalPolicyCreateReadPatch() {
}
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{
ID: 42,
@ -3697,24 +3690,26 @@ func (s *integrationEnterpriseTestSuite) TestTeamPolicyCreateReadPatch() {
createPol1 := &teamPolicyResponse{}
createPol1Req := &teamPolicyRequest{
Query: "query",
Name: "name1",
Description: "description",
Resolution: "resolution",
Platform: "linux",
Critical: true,
Query: "query",
Name: "name1",
Description: "description",
Resolution: "resolution",
Platform: "linux",
Critical: true,
CalendarEventsEnabled: true,
}
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/policies", team1.ID), createPol1Req, http.StatusOK, &createPol1)
allEqual(s.T(), createPol1Req, createPol1.Policy, fields...)
createPol2 := &teamPolicyResponse{}
createPol2Req := &teamPolicyRequest{
Query: "query",
Name: "name2",
Description: "description",
Resolution: "resolution",
Platform: "linux",
Critical: false,
Query: "query",
Name: "name2",
Description: "description",
Resolution: "resolution",
Platform: "linux",
Critical: false,
CalendarEventsEnabled: false,
}
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/policies", team1.ID), createPol2Req, http.StatusOK, &createPol2)
allEqual(s.T(), createPol2Req, createPol2.Policy, fields...)
@ -3730,12 +3725,13 @@ func (s *integrationEnterpriseTestSuite) TestTeamPolicyCreateReadPatch() {
patchPol1Req := &modifyTeamPolicyRequest{
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
Name: ptr.String("newName1"),
Query: ptr.String("newQuery"),
Description: ptr.String("newDescription"),
Resolution: ptr.String("newResolution"),
Platform: ptr.String("windows"),
Critical: ptr.Bool(false),
Name: ptr.String("newName1"),
Query: ptr.String("newQuery"),
Description: ptr.String("newDescription"),
Resolution: ptr.String("newResolution"),
Platform: ptr.String("windows"),
Critical: ptr.Bool(false),
CalendarEventsEnabled: ptr.Bool(false),
},
}
patchPol1 := &modifyTeamPolicyResponse{}
@ -3744,12 +3740,13 @@ func (s *integrationEnterpriseTestSuite) TestTeamPolicyCreateReadPatch() {
patchPol2Req := &modifyTeamPolicyRequest{
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
Name: ptr.String("newName2"),
Query: ptr.String("newQuery"),
Description: ptr.String("newDescription"),
Resolution: ptr.String("newResolution"),
Platform: ptr.String("windows"),
Critical: ptr.Bool(true),
Name: ptr.String("newName2"),
Query: ptr.String("newQuery"),
Description: ptr.String("newDescription"),
Resolution: ptr.String("newResolution"),
Platform: ptr.String("windows"),
Critical: ptr.Bool(true),
CalendarEventsEnabled: ptr.Bool(true),
},
}
patchPol2 := &modifyTeamPolicyResponse{}

View File

@ -20,14 +20,15 @@ import (
/////////////////////////////////////////////////////////////////////////////////
type teamPolicyRequest struct {
TeamID uint `url:"team_id"`
QueryID *uint `json:"query_id"`
Query string `json:"query"`
Name string `json:"name"`
Description string `json:"description"`
Resolution string `json:"resolution"`
Platform string `json:"platform"`
Critical bool `json:"critical" premium:"true"`
TeamID uint `url:"team_id"`
QueryID *uint `json:"query_id"`
Query string `json:"query"`
Name string `json:"name"`
Description string `json:"description"`
Resolution string `json:"resolution"`
Platform string `json:"platform"`
Critical bool `json:"critical" premium:"true"`
CalendarEventsEnabled bool `json:"calendar_events_enabled"`
}
type teamPolicyResponse struct {
@ -40,13 +41,14 @@ func (r teamPolicyResponse) error() error { return r.Err }
func teamPolicyEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*teamPolicyRequest)
resp, err := svc.NewTeamPolicy(ctx, req.TeamID, fleet.PolicyPayload{
QueryID: req.QueryID,
Name: req.Name,
Query: req.Query,
Description: req.Description,
Resolution: req.Resolution,
Platform: req.Platform,
Critical: req.Critical,
QueryID: req.QueryID,
Name: req.Name,
Query: req.Query,
Description: req.Description,
Resolution: req.Resolution,
Platform: req.Platform,
Critical: req.Critical,
CalendarEventsEnabled: req.CalendarEventsEnabled,
})
if 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 {
policy.Critical = *p.Critical
}
if p.CalendarEventsEnabled != nil {
policy.CalendarEventsEnabled = *p.CalendarEventsEnabled
}
logging.WithExtras(ctx, "name", policy.Name, "sql", policy.Query)
err = svc.ds.SavePolicy(ctx, policy, shouldRemoveAll)

View File

@ -124,7 +124,8 @@ func TestTriggerFailingPoliciesWebhookBasic(t *testing.T) {
"passing_host_count": 0,
"failing_host_count": 0,
"host_count_updated_at": null,
"critical": true
"critical": true,
"calendar_events_enabled": false
},
"hosts": [
{
@ -183,16 +184,17 @@ func TestTriggerFailingPoliciesWebhookTeam(t *testing.T) {
policiesByID := map[uint]*fleet.Policy{
1: {
PolicyData: fleet.PolicyData{
ID: 1,
Name: "policy1",
Query: "select 1",
Description: "policy1 description",
AuthorID: ptr.Uint(1),
AuthorName: "Alice",
AuthorEmail: "alice@example.com",
TeamID: &teamID,
Resolution: ptr.String("policy1 resolution"),
Platform: "darwin",
ID: 1,
Name: "policy1",
Query: "select 1",
Description: "policy1 description",
AuthorID: ptr.Uint(1),
AuthorName: "Alice",
AuthorEmail: "alice@example.com",
TeamID: &teamID,
Resolution: ptr.String("policy1 resolution"),
Platform: "darwin",
CalendarEventsEnabled: true,
},
},
2: {
@ -309,7 +311,8 @@ func TestTriggerFailingPoliciesWebhookTeam(t *testing.T) {
"passing_host_count": 0,
"failing_host_count": 0,
"host_count_updated_at": null,
"critical": false
"critical": false,
"calendar_events_enabled": true
},
"hosts": [
{