diff --git a/changes/21467-policies-for-no-team b/changes/21467-policies-for-no-team new file mode 100644 index 000000000000..4613cd39edaf --- /dev/null +++ b/changes/21467-policies-for-no-team @@ -0,0 +1 @@ +* Added support for policies in "No team" that run on hosts that belong to "No team". diff --git a/cmd/fleetctl/gitops.go b/cmd/fleetctl/gitops.go index b593ebf9299b..fc9e3c7a8374 100644 --- a/cmd/fleetctl/gitops.go +++ b/cmd/fleetctl/gitops.go @@ -77,6 +77,23 @@ func gitopsCommand() *cli.Command { if appConfig.License == nil { return errors.New("no license struct found in app config") } + logf := func(format string, a ...interface{}) { + _, _ = fmt.Fprintf(c.App.Writer, format, a...) + } + + // We need to extract the controls from no-team.yml to be able to apply them when applying the global app config. + var noTeamControls spec.Controls + for _, flFilename := range flFilenames.Value() { + if filepath.Base(flFilename) == "no-team.yml" { + baseDir := filepath.Dir(flFilename) + config, err := spec.GitOpsFromFile(flFilename, baseDir, appConfig, logf) + if err != nil { + return err + } + noTeamControls = config.Controls + break + } + } var originalABMConfig []any var originalVPPConfig []any @@ -92,7 +109,7 @@ func gitopsCommand() *cli.Command { secrets := make(map[string]struct{}) for _, flFilename := range flFilenames.Value() { baseDir := filepath.Dir(flFilename) - config, err := spec.GitOpsFromFile(flFilename, baseDir, appConfig) + config, err := spec.GitOpsFromFile(flFilename, baseDir, appConfig, logf) if err != nil { return err } @@ -109,6 +126,21 @@ func gitopsCommand() *cli.Command { firstFileMustBeGlobal = ptr.Bool(false) } + if isGlobalConfig { + if noTeamControls.Set() && config.Controls.Set() { + return errors.New("'controls' cannot be set on both global config and on no-team.yml") + } + if !noTeamControls.Defined && !config.Controls.Defined { + if appConfig.License.IsPremium() { + return errors.New("'controls' must be set on global config or no-team.yml") + } + return errors.New("'controls' must be set on global config") + } + if !config.Controls.Set() { + config.Controls = noTeamControls + } + } + // Special handling for tokens is required because they link to teams (by // name.) Because teams can be created/deleted during the same gitops run, we // grab some information to help us determine allowed/restricted actions and @@ -160,9 +192,6 @@ func gitopsCommand() *cli.Command { } } } - logf := func(format string, a ...interface{}) { - _, _ = fmt.Fprintf(c.App.Writer, format, a...) - } if flDryRun { incomingSecrets := fleetClient.GetGitOpsSecrets(config) for _, secret := range incomingSecrets { diff --git a/cmd/fleetctl/gitops_test.go b/cmd/fleetctl/gitops_test.go index 58fb94b1c62a..a0153097f009 100644 --- a/cmd/fleetctl/gitops_test.go +++ b/cmd/fleetctl/gitops_test.go @@ -13,6 +13,7 @@ import ( "testing" "time" + "github.com/fleetdm/fleet/v4/pkg/file" "github.com/fleetdm/fleet/v4/server/config" "github.com/fleetdm/fleet/v4/server/datastore/mysql" "github.com/fleetdm/fleet/v4/server/fleet" @@ -141,6 +142,28 @@ org_settings: require.Error(t, err) assert.Contains(t, err.Error(), "organization name must be present") + // Missing controls. + tmpFile2, err := os.CreateTemp(t.TempDir(), "*.yml") + require.NoError(t, err) + _, err = tmpFile2.WriteString( + ` +queries: +policies: +agent_options: +org_settings: + server_settings: + server_url: https://example.com + org_info: + contact_url: https://example.com/contact + org_name: Foobar + secrets: +`, + ) + require.NoError(t, err) + _, err = runAppNoChecks([]string{"gitops", "-f", tmpFile2.Name()}) + require.Error(t, err) + assert.Equal(t, `'controls' must be set on global config`, err.Error()) + // Dry run t.Setenv("ORG_NAME", orgName) _ = runAppForTest(t, []string{"gitops", "-f", tmpFile.Name(), "--dry-run"}) @@ -398,16 +421,15 @@ software: require.Error(t, err) assert.Contains(t, err.Error(), "'name' is required") - // reserved team name; should error in both dry run and real + // Invalid name for "No team" file (dry and real). t.Setenv("TEST_TEAM_NAME", "no TEam") _, err = runAppNoChecks([]string{"gitops", "-f", tmpFile.Name(), "--dry-run"}) require.Error(t, err) - assert.Contains(t, err.Error(), `"No team" is a reserved team name`) - + assert.Contains(t, err.Error(), fmt.Sprintf("file %q for 'No team' must be named 'no-team.yml'", tmpFile.Name())) t.Setenv("TEST_TEAM_NAME", "no TEam") _, err = runAppNoChecks([]string{"gitops", "-f", tmpFile.Name()}) require.Error(t, err) - assert.Contains(t, err.Error(), `"No team" is a reserved team name`) + assert.Contains(t, err.Error(), fmt.Sprintf("file %q for 'No team' must be named 'no-team.yml'", tmpFile.Name())) t.Setenv("TEST_TEAM_NAME", "All teams") _, err = runAppNoChecks([]string{"gitops", "-f", tmpFile.Name(), "--dry-run"}) @@ -1164,6 +1186,336 @@ software: assert.True(t, ds.DeleteTeamFuncInvoked) } +func TestGitOpsBasicGlobalAndNoTeam(t *testing.T) { + // Cannot run t.Parallel() because runServerWithMockedDS sets the FLEET_SERVER_ADDRESS + // environment variable. + + license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)} + _, ds := runServerWithMockedDS( + t, &service.TestServerOpts{ + License: license, + }, + ) + // Mock appConfig + savedAppConfig := &fleet.AppConfig{} + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return &fleet.AppConfig{}, nil + } + ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error { + savedAppConfig = config + return nil + } + ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error { + return nil + } + ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error { + return nil + } + + const ( + fleetServerURL = "https://fleet.example.com" + orgName = "GitOps Test" + secret = "TestSecret" + ) + var enrolledSecrets []*fleet.EnrollSecret + var enrolledTeamSecrets []*fleet.EnrollSecret + var savedTeam *fleet.Team + team := &fleet.Team{ + ID: 1, + CreatedAt: time.Now(), + Name: teamName, + } + + ds.IsEnrollSecretAvailableFunc = func(ctx context.Context, secret string, new bool, teamID *uint) (bool, error) { + return true, nil + } + ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error { + if teamID == nil { + enrolledSecrets = secrets + } else { + enrolledTeamSecrets = secrets + } + return nil + } + ds.BatchSetMDMProfilesFunc = func( + ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, + macDecls []*fleet.MDMAppleDeclaration, + ) (updates fleet.MDMProfilesUpdates, err error) { + assert.Empty(t, macProfiles) + assert.Empty(t, winProfiles) + return fleet.MDMProfilesUpdates{}, nil + } + ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error { + assert.Empty(t, scripts) + return nil + } + ds.BulkSetPendingMDMHostProfilesFunc = func( + ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string, + ) (updates fleet.MDMProfilesUpdates, err error) { + assert.Empty(t, profileUUIDs) + return fleet.MDMProfilesUpdates{}, nil + } + ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error { + return nil + } + ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) { + require.ElementsMatch(t, labels, []string{fleet.BuiltinLabelMacOS14Plus}) + return map[string]uint{fleet.BuiltinLabelMacOS14Plus: 1}, nil + } + ds.ListGlobalPoliciesFunc = func(ctx context.Context, opts fleet.ListOptions) ([]*fleet.Policy, error) { return nil, nil } + ds.ListTeamPoliciesFunc = func( + ctx context.Context, teamID uint, opts fleet.ListOptions, iopts fleet.ListOptions, + ) (teamPolicies []*fleet.Policy, inheritedPolicies []*fleet.Policy, err error) { + return nil, nil, nil + } + ds.ListTeamsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.ListOptions) ([]*fleet.Team, error) { + return nil, nil + } + ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) { return nil, nil } + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { + return nil + } + ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) { + job.ID = 1 + return job, nil + } + ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) { + if tid == team.ID { + return savedTeam, nil + } + return nil, nil + } + ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) { + if name == teamName && savedTeam != nil { + return savedTeam, nil + } + return nil, ¬FoundError{} + } + ds.TeamByFilenameFunc = func(ctx context.Context, filename string) (*fleet.Team, error) { + if savedTeam != nil && *savedTeam.Filename == filename { + return savedTeam, nil + } + return nil, ¬FoundError{} + } + ds.NewTeamFunc = func(ctx context.Context, newTeam *fleet.Team) (*fleet.Team, error) { + newTeam.ID = team.ID + savedTeam = newTeam + enrolledTeamSecrets = newTeam.Secrets + return newTeam, nil + } + ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) { + savedTeam = team + return team, nil + } + ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) ([]fleet.SoftwareInstaller, error) { + return nil, nil + } + ds.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions, tmFilter fleet.TeamFilter) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) { + return nil, 0, nil, nil + } + + globalFileBasic, err := os.CreateTemp(t.TempDir(), "*.yml") + require.NoError(t, err) + + _, err = globalFileBasic.WriteString(fmt.Sprintf( + ` +controls: +queries: +policies: +agent_options: +org_settings: + server_settings: + server_url: %s + org_info: + contact_url: https://example.com/contact + org_logo_url: "" + org_logo_url_light_background: "" + org_name: %s + secrets: [{"secret":"globalSecret"}] +software: +`, fleetServerURL, orgName), + ) + require.NoError(t, err) + + globalFileWithSoftware, err := os.CreateTemp(t.TempDir(), "*.yml") + require.NoError(t, err) + _, err = globalFileWithSoftware.WriteString(fmt.Sprintf( + ` +controls: +queries: +policies: +agent_options: +org_settings: + server_settings: + server_url: %s + org_info: + contact_url: https://example.com/contact + org_logo_url: "" + org_logo_url_light_background: "" + org_name: %s + secrets: [{"secret":"globalSecret"}] +software: + packages: + - url: https://example.com +`, fleetServerURL, orgName), + ) + require.NoError(t, err) + + globalFileWithControls, err := os.CreateTemp(t.TempDir(), "*.yml") + require.NoError(t, err) + _, err = globalFileWithControls.WriteString(fmt.Sprintf( + ` +controls: + ios_updates: + deadline: "2022-02-02" + minimum_version: "17.6" +queries: +policies: +agent_options: +org_settings: + server_settings: + server_url: %s + org_info: + contact_url: https://example.com/contact + org_logo_url: "" + org_logo_url_light_background: "" + org_name: %s + secrets: [{"secret":"globalSecret"}] +software: +`, fleetServerURL, orgName), + ) + require.NoError(t, err) + + globalFileWithoutControlsAndSoftwareKeys, err := os.CreateTemp(t.TempDir(), "*.yml") + require.NoError(t, err) + _, err = globalFileWithoutControlsAndSoftwareKeys.WriteString(fmt.Sprintf( + ` +queries: +policies: +agent_options: +org_settings: + server_settings: + server_url: %s + org_info: + contact_url: https://example.com/contact + org_logo_url: "" + org_logo_url_light_background: "" + org_name: %s + secrets: [{"secret":"globalSecret"}] +`, fleetServerURL, orgName), + ) + require.NoError(t, err) + + teamFile, err := os.CreateTemp(t.TempDir(), "*.yml") + require.NoError(t, err) + _, err = teamFile.WriteString(fmt.Sprintf(` +controls: +queries: +policies: +agent_options: +name: %s +team_settings: + secrets: [{"secret":"%s"}] +software: +`, teamName, secret), + ) + require.NoError(t, err) + + noTeamFilePath := filepath.Join(t.TempDir(), "no-team.yml") + noTeamFile, err := os.Create(noTeamFilePath) + require.NoError(t, err) + _, err = noTeamFile.WriteString(` +controls: +policies: +name: No team +software: +`) + require.NoError(t, err) + + noTeamFilePathWithControls := filepath.Join(t.TempDir(), "no-team.yml") + noTeamFileWithControls, err := os.Create(noTeamFilePathWithControls) + require.NoError(t, err) + _, err = noTeamFileWithControls.WriteString(` +controls: + ipados_updates: + deadline: "2023-03-03" + minimum_version: "18.0" +policies: +name: No team +software: +`) + require.NoError(t, err) + + noTeamFilePathWithoutControls := filepath.Join(t.TempDir(), "no-team.yml") + noTeamFileWithoutControls, err := os.Create(noTeamFilePathWithoutControls) + require.NoError(t, err) + _, err = noTeamFileWithoutControls.WriteString(` +policies: +name: No team +software: +`) + require.NoError(t, err) + + // Dry run, global defines software, should fail. + _, err = runAppNoChecks([]string{"gitops", "-f", globalFileWithSoftware.Name(), "-f", teamFile.Name(), "-f", noTeamFile.Name(), "--dry-run"}) + require.Error(t, err) + assert.True(t, strings.Contains(err.Error(), "'software' cannot be set on global file")) + // Real run, global defines software, should fail. + _, err = runAppNoChecks([]string{"gitops", "-f", globalFileWithSoftware.Name(), "-f", teamFile.Name(), "-f", noTeamFile.Name()}) + require.Error(t, err) + assert.True(t, strings.Contains(err.Error(), "'software' cannot be set on global file")) + + // Dry run, both global and no-team.yml define controls. + _, err = runAppNoChecks([]string{"gitops", "-f", globalFileWithControls.Name(), "-f", teamFile.Name(), "-f", noTeamFileWithControls.Name(), "--dry-run"}) + require.Error(t, err) + assert.True(t, strings.Contains(err.Error(), "'controls' cannot be set on both global config and on no-team.yml")) + // Real run, both global and no-team.yml define controls. + _, err = runAppNoChecks([]string{"gitops", "-f", globalFileWithControls.Name(), "-f", teamFile.Name(), "-f", noTeamFileWithControls.Name(), "--dry-run"}) + require.Error(t, err) + assert.True(t, strings.Contains(err.Error(), "'controls' cannot be set on both global config and on no-team.yml")) + + // Dry run, controls should be defined somewhere, either in no-team.yml or global. + _, err = runAppNoChecks([]string{"gitops", "-f", globalFileWithoutControlsAndSoftwareKeys.Name(), "-f", teamFile.Name(), "-f", noTeamFileWithoutControls.Name(), "--dry-run"}) + require.Error(t, err) + assert.True(t, strings.Contains(err.Error(), "'controls' must be set on global config or no-team.yml")) + // Real run, both global and no-team.yml define controls. + _, err = runAppNoChecks([]string{"gitops", "-f", globalFileWithoutControlsAndSoftwareKeys.Name(), "-f", teamFile.Name(), "-f", noTeamFileWithoutControls.Name(), "--dry-run"}) + require.Error(t, err) + assert.True(t, strings.Contains(err.Error(), "'controls' must be set on global config or no-team.yml")) + + // Dry run, global file without controls and software keys. + _ = runAppForTest(t, []string{"gitops", "-f", globalFileWithoutControlsAndSoftwareKeys.Name(), "-f", teamFile.Name(), "-f", noTeamFile.Name(), "--dry-run"}) + assert.Equal(t, fleet.AppConfig{}, *savedAppConfig, "AppConfig should be empty") + + // Real run, global file without controls and software keys. + _ = runAppForTest(t, []string{"gitops", "-f", globalFileWithoutControlsAndSoftwareKeys.Name(), "-f", teamFile.Name(), "-f", noTeamFile.Name()}) + assert.Equal(t, orgName, savedAppConfig.OrgInfo.OrgName) + assert.Equal(t, fleetServerURL, savedAppConfig.ServerSettings.ServerURL) + assert.Len(t, enrolledSecrets, 1) + require.NotNil(t, savedTeam) + assert.Equal(t, teamName, savedTeam.Name) + require.Len(t, enrolledTeamSecrets, 1) + assert.Equal(t, secret, enrolledTeamSecrets[0].Secret) + + // Restore to test below. + savedAppConfig = &fleet.AppConfig{} + + // Dry run + _ = runAppForTest(t, []string{"gitops", "-f", globalFileBasic.Name(), "-f", teamFile.Name(), "-f", noTeamFile.Name(), "--dry-run"}) + assert.Equal(t, fleet.AppConfig{}, *savedAppConfig, "AppConfig should be empty") + // Real run + _ = runAppForTest(t, []string{"gitops", "-f", globalFileBasic.Name(), "-f", teamFile.Name(), "-f", noTeamFile.Name()}) + assert.Equal(t, orgName, savedAppConfig.OrgInfo.OrgName) + assert.Equal(t, fleetServerURL, savedAppConfig.ServerSettings.ServerURL) + assert.Len(t, enrolledSecrets, 1) + require.NotNil(t, savedTeam) + assert.Equal(t, teamName, savedTeam.Name) + require.Len(t, enrolledTeamSecrets, 1) + assert.Equal(t, secret, enrolledTeamSecrets[0].Secret) +} + func TestGitOpsFullGlobalAndTeam(t *testing.T) { // Cannot run t.Parallel() because it sets environment variables // mdm test configuration must be set so that activating windows MDM works. @@ -1299,8 +1651,8 @@ func TestGitOpsNoTeamSoftwareInstallers(t *testing.T) { startSoftwareInstallerServer(t) cases := []struct { - file string - wantErr string + noTeamFile string + wantErr string }{ {"testdata/gitops/no_team_software_installer_not_found.yml", "Please make sure that URLs are publicy accessible to the internet."}, {"testdata/gitops/no_team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe or .deb."}, @@ -1314,11 +1666,18 @@ func TestGitOpsNoTeamSoftwareInstallers(t *testing.T) { {"testdata/gitops/no_team_software_installer_invalid_self_service_value.yml", "\"packages.self_service\" must be a bool, found string"}, } for _, c := range cases { - t.Run(filepath.Base(c.file), func(t *testing.T) { + t.Run(filepath.Base(c.noTeamFile), func(t *testing.T) { setupFullGitOpsPremiumServer(t) t.Setenv("APPLE_BM_DEFAULT_TEAM", "") - _, err := runAppNoChecks([]string{"gitops", "-f", c.file}) + globalFile := "./testdata/gitops/global_config_no_paths.yml" + dstPath := filepath.Join(filepath.Dir(c.noTeamFile), "no-team.yml") + t.Cleanup(func() { + os.Remove(dstPath) + }) + err := file.Copy(c.noTeamFile, dstPath, 0o755) + require.NoError(t, err) + _, err = runAppNoChecks([]string{"gitops", "-f", globalFile, "-f", dstPath}) if c.wantErr == "" { require.NoError(t, err) } else { diff --git a/cmd/fleetctl/testdata/gitops/no_team_software_installer_install_not_found.yml b/cmd/fleetctl/testdata/gitops/no_team_software_installer_install_not_found.yml index d3bcada54e82..58bae27ae971 100644 --- a/cmd/fleetctl/testdata/gitops/no_team_software_installer_install_not_found.yml +++ b/cmd/fleetctl/testdata/gitops/no_team_software_installer_install_not_found.yml @@ -1,19 +1,8 @@ -# Test config +name: No team controls: -queries: policies: -agent_options: -org_settings: - server_settings: - server_url: $FLEET_SERVER_URL - org_info: - contact_url: https://example.com/contact - org_logo_url: "" - org_logo_url_light_background: "" - org_name: ${ORG_NAME} - secrets: [{"secret":"globalSecret"}] software: packages: - url: ${SOFTWARE_INSTALLER_URL}/ruby.deb install_script: - path: lib/notfound.sh \ No newline at end of file + path: lib/notfound.sh diff --git a/cmd/fleetctl/testdata/gitops/no_team_software_installer_invalid_self_service_value.yml b/cmd/fleetctl/testdata/gitops/no_team_software_installer_invalid_self_service_value.yml index acee06d683a7..b333e7816e48 100644 --- a/cmd/fleetctl/testdata/gitops/no_team_software_installer_invalid_self_service_value.yml +++ b/cmd/fleetctl/testdata/gitops/no_team_software_installer_invalid_self_service_value.yml @@ -1,18 +1,7 @@ -# Test config +name: No team controls: -queries: policies: -agent_options: -org_settings: - server_settings: - server_url: $FLEET_SERVER_URL - org_info: - contact_url: https://example.com/contact - org_logo_url: "" - org_logo_url_light_background: "" - org_name: ${ORG_NAME} - secrets: [{"secret":"globalSecret"}] software: packages: - url: ${SOFTWARE_INSTALLER_URL}/invalidtype.txt - self_service: "not a boolean" \ No newline at end of file + self_service: "not a boolean" diff --git a/cmd/fleetctl/testdata/gitops/no_team_software_installer_no_url.yml b/cmd/fleetctl/testdata/gitops/no_team_software_installer_no_url.yml index 6d83a9daed50..d897af7b4363 100644 --- a/cmd/fleetctl/testdata/gitops/no_team_software_installer_no_url.yml +++ b/cmd/fleetctl/testdata/gitops/no_team_software_installer_no_url.yml @@ -1,17 +1,6 @@ -# Test config +name: No TEAM controls: -queries: policies: -agent_options: -org_settings: - server_settings: - server_url: $FLEET_SERVER_URL - org_info: - contact_url: https://example.com/contact - org_logo_url: "" - org_logo_url_light_background: "" - org_name: ${ORG_NAME} - secrets: [{"secret":"globalSecret"}] software: packages: - install_script: @@ -19,4 +8,4 @@ software: pre_install_query: path: lib/query_ruby.yml post_install_script: - path: lib/post_install_ruby.sh \ No newline at end of file + path: lib/post_install_ruby.sh diff --git a/cmd/fleetctl/testdata/gitops/no_team_software_installer_not_found.yml b/cmd/fleetctl/testdata/gitops/no_team_software_installer_not_found.yml index cd7332f91e56..590458e78b6c 100644 --- a/cmd/fleetctl/testdata/gitops/no_team_software_installer_not_found.yml +++ b/cmd/fleetctl/testdata/gitops/no_team_software_installer_not_found.yml @@ -1,17 +1,6 @@ -# Test config +name: No team controls: -queries: policies: -agent_options: -org_settings: - server_settings: - server_url: $FLEET_SERVER_URL - org_info: - contact_url: https://example.com/contact - org_logo_url: "" - org_logo_url_light_background: "" - org_name: ${ORG_NAME} - secrets: [{"secret":"globalSecret"}] software: packages: - - url: ${SOFTWARE_INSTALLER_URL}/notfound.deb \ No newline at end of file + - url: ${SOFTWARE_INSTALLER_URL}/notfound.deb diff --git a/cmd/fleetctl/testdata/gitops/no_team_software_installer_post_install_not_found.yml b/cmd/fleetctl/testdata/gitops/no_team_software_installer_post_install_not_found.yml index ac0a436360ca..12b2598d59c6 100644 --- a/cmd/fleetctl/testdata/gitops/no_team_software_installer_post_install_not_found.yml +++ b/cmd/fleetctl/testdata/gitops/no_team_software_installer_post_install_not_found.yml @@ -1,21 +1,10 @@ -# Test config +name: No team controls: -queries: policies: -agent_options: -org_settings: - server_settings: - server_url: $FLEET_SERVER_URL - org_info: - contact_url: https://example.com/contact - org_logo_url: "" - org_logo_url_light_background: "" - org_name: ${ORG_NAME} - secrets: [{"secret":"globalSecret"}] software: packages: - url: ${SOFTWARE_INSTALLER_URL}/ruby.deb install_script: path: lib/install_ruby.sh post_install_script: - path: lib/notfound.sh \ No newline at end of file + path: lib/notfound.sh diff --git a/cmd/fleetctl/testdata/gitops/no_team_software_installer_pre_condition_multiple_queries.yml b/cmd/fleetctl/testdata/gitops/no_team_software_installer_pre_condition_multiple_queries.yml index a2b5419c056a..15ddcb438c39 100644 --- a/cmd/fleetctl/testdata/gitops/no_team_software_installer_pre_condition_multiple_queries.yml +++ b/cmd/fleetctl/testdata/gitops/no_team_software_installer_pre_condition_multiple_queries.yml @@ -1,17 +1,6 @@ -# Test config +name: No team controls: -queries: policies: -agent_options: -org_settings: - server_settings: - server_url: $FLEET_SERVER_URL - org_info: - contact_url: https://example.com/contact - org_logo_url: "" - org_logo_url_light_background: "" - org_name: ${ORG_NAME} - secrets: [{"secret":"globalSecret"}] software: packages: - url: ${SOFTWARE_INSTALLER_URL}/ruby.deb @@ -20,4 +9,4 @@ software: pre_install_query: path: lib/query_multiple.yml post_install_script: - path: lib/post_install_ruby.sh \ No newline at end of file + path: lib/post_install_ruby.sh diff --git a/cmd/fleetctl/testdata/gitops/no_team_software_installer_pre_condition_not_found.yml b/cmd/fleetctl/testdata/gitops/no_team_software_installer_pre_condition_not_found.yml index bafde42691b5..48e6ff42e5d5 100644 --- a/cmd/fleetctl/testdata/gitops/no_team_software_installer_pre_condition_not_found.yml +++ b/cmd/fleetctl/testdata/gitops/no_team_software_installer_pre_condition_not_found.yml @@ -1,21 +1,10 @@ -# Test config +name: No team controls: -queries: policies: -agent_options: -org_settings: - server_settings: - server_url: $FLEET_SERVER_URL - org_info: - contact_url: https://example.com/contact - org_logo_url: "" - org_logo_url_light_background: "" - org_name: ${ORG_NAME} - secrets: [{"secret":"globalSecret"}] software: packages: - url: ${SOFTWARE_INSTALLER_URL}/ruby.deb install_script: path: lib/install_ruby.sh pre_install_query: - path: lib/notfound.yml \ No newline at end of file + path: lib/notfound.yml diff --git a/cmd/fleetctl/testdata/gitops/no_team_software_installer_too_large.yml b/cmd/fleetctl/testdata/gitops/no_team_software_installer_too_large.yml index db4ffd32113a..23ba8dbe8064 100644 --- a/cmd/fleetctl/testdata/gitops/no_team_software_installer_too_large.yml +++ b/cmd/fleetctl/testdata/gitops/no_team_software_installer_too_large.yml @@ -1,17 +1,6 @@ -# Test config +name: No team controls: -queries: policies: -agent_options: -org_settings: - server_settings: - server_url: $FLEET_SERVER_URL - org_info: - contact_url: https://example.com/contact - org_logo_url: "" - org_logo_url_light_background: "" - org_name: ${ORG_NAME} - secrets: [{"secret":"globalSecret"}] software: packages: - - url: ${SOFTWARE_INSTALLER_URL}/toolarge.deb \ No newline at end of file + - url: ${SOFTWARE_INSTALLER_URL}/toolarge.deb diff --git a/cmd/fleetctl/testdata/gitops/no_team_software_installer_unsupported.yml b/cmd/fleetctl/testdata/gitops/no_team_software_installer_unsupported.yml index 2bc609b931d7..ace876a8d53d 100644 --- a/cmd/fleetctl/testdata/gitops/no_team_software_installer_unsupported.yml +++ b/cmd/fleetctl/testdata/gitops/no_team_software_installer_unsupported.yml @@ -1,17 +1,6 @@ -# Test config +name: "No team" controls: -queries: policies: -agent_options: -org_settings: - server_settings: - server_url: $FLEET_SERVER_URL - org_info: - contact_url: https://example.com/contact - org_logo_url: "" - org_logo_url_light_background: "" - org_name: ${ORG_NAME} - secrets: [{"secret":"globalSecret"}] software: packages: - - url: ${SOFTWARE_INSTALLER_URL}/invalidtype.txt \ No newline at end of file + - url: ${SOFTWARE_INSTALLER_URL}/invalidtype.txt diff --git a/cmd/fleetctl/testdata/gitops/no_team_software_installer_valid.yml b/cmd/fleetctl/testdata/gitops/no_team_software_installer_valid.yml index e0fcaa490ec0..db8043baf9be 100644 --- a/cmd/fleetctl/testdata/gitops/no_team_software_installer_valid.yml +++ b/cmd/fleetctl/testdata/gitops/no_team_software_installer_valid.yml @@ -1,17 +1,6 @@ -# Test config +name: No team controls: -queries: policies: -agent_options: -org_settings: - server_settings: - server_url: $FLEET_SERVER_URL - org_info: - contact_url: https://example.com/contact - org_logo_url: "" - org_logo_url_light_background: "" - org_name: ${ORG_NAME} - secrets: [{"secret":"globalSecret"}] software: packages: - url: ${SOFTWARE_INSTALLER_URL}/ruby.deb @@ -22,4 +11,4 @@ software: post_install_script: path: lib/post_install_ruby.sh - url: ${SOFTWARE_INSTALLER_URL}/other.deb - self_service: true \ No newline at end of file + self_service: true diff --git a/ee/server/service/vpp.go b/ee/server/service/vpp.go index b03291ff4693..d1f13bd55589 100644 --- a/ee/server/service/vpp.go +++ b/ee/server/service/vpp.go @@ -149,8 +149,8 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string, vppAppTeams = append(vppAppTeams, app.VPPAppTeam) } } - } + } if err := svc.ds.SetTeamVPPApps(ctx, &team.ID, vppAppTeams); err != nil { if errors.Is(err, sql.ErrNoRows) { return fleet.NewUserMessageError(ctxerr.Wrap(ctx, err, "no vpp token to set team vpp assets"), http.StatusUnprocessableEntity) @@ -375,7 +375,7 @@ func getVPPAppsMetadata(ctx context.Context, ids []fleet.VPPAppTeam) ([]*fleet.V var apps []*fleet.VPPApp // Map of adamID to platform, then to whether it's available as self-service. - var adamIDMap = make(map[string]map[fleet.AppleDevicePlatform]bool) + adamIDMap := make(map[string]map[fleet.AppleDevicePlatform]bool) for _, id := range ids { if _, ok := adamIDMap[id.AdamID]; !ok { adamIDMap[id.AdamID] = make(map[fleet.AppleDevicePlatform]bool, 1) diff --git a/pkg/spec/gitops.go b/pkg/spec/gitops.go index 558d7a1f06c6..0d15687da72b 100644 --- a/pkg/spec/gitops.go +++ b/pkg/spec/gitops.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "slices" + "strings" "unicode" "github.com/fleetdm/fleet/v4/server/fleet" @@ -36,6 +37,16 @@ type Controls struct { EnableDiskEncryption interface{} `json:"enable_disk_encryption"` Scripts []BaseItem `json:"scripts"` + + Defined bool +} + +func (c Controls) Set() bool { + return c.MacOSUpdates != nil || c.IOSUpdates != nil || + c.IPadOSUpdates != nil || c.MacOSSettings != nil || + c.MacOSSetup != nil || c.MacOSMigration != nil || + c.WindowsUpdates != nil || c.WindowsSettings != nil || c.WindowsEnabledAndConfigured != nil || + c.EnableDiskEncryption != nil || len(c.Scripts) > 0 } type Policy struct { @@ -88,8 +99,10 @@ type GitOpsSoftware struct { AppStoreApps []*fleet.TeamSpecAppStoreApp } +type Logf func(format string, a ...interface{}) + // GitOpsFromFile parses a GitOps yaml file. -func GitOpsFromFile(filePath, baseDir string, appConfig *fleet.EnrichedAppConfig) (*GitOps, error) { +func GitOpsFromFile(filePath, baseDir string, appConfig *fleet.EnrichedAppConfig, logFn Logf) (*GitOps, error) { b, err := os.ReadFile(filePath) if err != nil { return nil, fmt.Errorf("failed to read file: %s: %w", filePath, err) @@ -126,17 +139,30 @@ func GitOpsFromFile(filePath, baseDir string, appConfig *fleet.EnrichedAppConfig } else { multiError = parseOrgSettings(orgSettingsRaw, result, baseDir, multiError) } - } else if teamOk && teamSettingsOk { + } else if teamOk { multiError = parseName(teamRaw, result, multiError) - multiError = parseTeamSettings(teamSettingsRaw, result, baseDir, multiError) + if result.IsNoTeam() { + if teamSettingsOk { + multiError = multierror.Append(multiError, fmt.Errorf("cannot set 'team_settings' on 'No team' file: %q", filePath)) + } + if filepath.Base(filePath) != "no-team.yml" { + multiError = multierror.Append(multiError, fmt.Errorf("file %q for 'No team' must be named 'no-team.yml'", filePath)) + } + } else { + if !teamSettingsOk { + multiError = multierror.Append(multiError, errors.New("'team_settings' is required when 'name' is provided")) + } else { + multiError = parseTeamSettings(teamSettingsRaw, result, baseDir, multiError) + } + } } else { multiError = multierror.Append(multiError, errors.New("either 'org_settings' or 'name' and 'team_settings' is required")) } // Validate the required top level options multiError = parseControls(top, result, baseDir, multiError) - multiError = parseAgentOptions(top, result, baseDir, multiError) - multiError = parseQueries(top, result, baseDir, multiError) + multiError = parseAgentOptions(top, result, baseDir, logFn, multiError) + multiError = parseQueries(top, result, baseDir, logFn, multiError) if appConfig != nil && appConfig.License.IsPremium() { multiError = parseSoftware(top, result, baseDir, multiError) @@ -161,6 +187,20 @@ func parseName(raw json.RawMessage, result *GitOps, multiError *multierror.Error return multiError } +func (g *GitOps) global() bool { + return g.TeamName == nil || *g.TeamName == "" +} + +func (g *GitOps) IsNoTeam() bool { + return g.TeamName != nil && isNoTeam(*g.TeamName) +} + +func isNoTeam(teamName string) bool { + return strings.ToLower(teamName) == strings.ToLower(noTeam) +} + +const noTeam = "No team" + func parseOrgSettings(raw json.RawMessage, result *GitOps, baseDir string, multiError *multierror.Error) *multierror.Error { var orgSettingsTop BaseItem if err := json.Unmarshal(raw, &orgSettingsTop); err != nil { @@ -314,9 +354,14 @@ func parseSecrets(result *GitOps, multiError *multierror.Error) *multierror.Erro return multiError } -func parseAgentOptions(top map[string]json.RawMessage, result *GitOps, baseDir string, multiError *multierror.Error) *multierror.Error { +func parseAgentOptions(top map[string]json.RawMessage, result *GitOps, baseDir string, logFn Logf, multiError *multierror.Error) *multierror.Error { agentOptionsRaw, ok := top["agent_options"] - if !ok { + if result.IsNoTeam() { + if ok { + logFn("[!] 'agent_options' is not supported for \"No team\". This key will be ignored.") + } + return multiError + } else if !ok { return multierror.Append(multiError, errors.New("'agent_options' is required")) } var agentOptionsTop BaseItem @@ -366,12 +411,14 @@ func parseAgentOptions(top map[string]json.RawMessage, result *GitOps, baseDir s func parseControls(top map[string]json.RawMessage, result *GitOps, baseDir string, multiError *multierror.Error) *multierror.Error { controlsRaw, ok := top["controls"] if !ok { - return multierror.Append(multiError, errors.New("'controls' is required")) + // Nothing to do, return. + return multiError } var controlsTop Controls if err := json.Unmarshal(controlsRaw, &controlsTop); err != nil { return multierror.Append(multiError, fmt.Errorf("failed to unmarshal controls: %v", err)) } + controlsTop.Defined = true if controlsTop.Path == nil { result.Controls = controlsTop } else { @@ -516,9 +563,14 @@ func parsePolicyInstallSoftware(baseDir string, teamName *string, policy *Policy return nil } -func parseQueries(top map[string]json.RawMessage, result *GitOps, baseDir string, multiError *multierror.Error) *multierror.Error { +func parseQueries(top map[string]json.RawMessage, result *GitOps, baseDir string, logFn Logf, multiError *multierror.Error) *multierror.Error { queriesRaw, ok := top["queries"] - if !ok { + if result.IsNoTeam() { + if ok { + logFn("[!] 'queries' is not supported for \"No team\". This key will be ignored.") + } + return multiError + } else if !ok { return multierror.Append(multiError, errors.New("'queries' key is required")) } var queries []Query @@ -593,7 +645,11 @@ func parseQueries(top map[string]json.RawMessage, result *GitOps, baseDir string func parseSoftware(top map[string]json.RawMessage, result *GitOps, baseDir string, multiError *multierror.Error) *multierror.Error { softwareRaw, ok := top["software"] - if !ok { + if result.global() { + if ok && string(softwareRaw) != "null" { + return multierror.Append(multiError, errors.New("'software' cannot be set on global file")) + } + } else if !ok { return multierror.Append(multiError, errors.New("'software' is required")) } var software Software diff --git a/pkg/spec/gitops_test.go b/pkg/spec/gitops_test.go index ea01fcf1dc0e..1fa969910272 100644 --- a/pkg/spec/gitops_test.go +++ b/pkg/spec/gitops_test.go @@ -53,9 +53,22 @@ func createTempFile(t *testing.T, pattern, contents string) (filePath string, ba return tmpFile.Name(), filepath.Dir(tmpFile.Name()) } +func createNamedFileOnTempDir(t *testing.T, name string, contents string) (filePath string, baseDir string) { + tmpFilePath := filepath.Join(t.TempDir(), name) + tmpFile, err := os.Create(tmpFilePath) + require.NoError(t, err) + _, err = tmpFile.WriteString(contents) + require.NoError(t, err) + require.NoError(t, tmpFile.Close()) + return tmpFile.Name(), filepath.Dir(tmpFile.Name()) +} + func gitOpsFromString(t *testing.T, s string) (*GitOps, error) { path, basePath := createTempFile(t, "", s) - return GitOpsFromFile(path, basePath, nil) + return GitOpsFromFile(path, basePath, nil, nopLogf) +} + +func nopLogf(_ string, _ ...interface{}) { } func TestValidGitOpsYaml(t *testing.T) { @@ -118,7 +131,7 @@ func TestValidGitOpsYaml(t *testing.T) { } } - gitops, err := GitOpsFromFile(test.filePath, "./testdata", appConfig) + gitops, err := GitOpsFromFile(test.filePath, "./testdata", appConfig, nopLogf) require.NoError(t, err) if test.isTeam { @@ -443,14 +456,44 @@ func TestInvalidGitOpsYaml(t *testing.T) { _, err = gitOpsFromString(t, config) assert.ErrorContains(t, err, "must have a 'secret' key") + // Missing team_settings. + config = getConfig([]string{"team_settings"}) + _, err = gitOpsFromString(t, config) + assert.ErrorContains(t, err, "'team_settings' is required when 'name' is provided") + + // team_settings set on a "no-team.yml". + config = getConfig([]string{"name"}) + config += "name: No team\n" + noTeamPath1, noTeamBasePath1 := createNamedFileOnTempDir(t, "no-team.yml", config) + _, err = GitOpsFromFile(noTeamPath1, noTeamBasePath1, nil, nopLogf) + assert.ErrorContains(t, err, fmt.Sprintf("cannot set 'team_settings' on 'No team' file: %q", noTeamPath1)) + + // 'No team' file with invalid name. + config = getConfig([]string{"name", "team_settings"}) + config += "name: No team\n" + noTeamPath2, noTeamBasePath2 := createNamedFileOnTempDir(t, "foobar.yml", config) + _, err = GitOpsFromFile(noTeamPath2, noTeamBasePath2, nil, nopLogf) + assert.ErrorContains(t, err, fmt.Sprintf("file %q for 'No team' must be named 'no-team.yml'", noTeamPath2)) + // Missing secrets config = getConfig([]string{"team_settings"}) config += "team_settings:\n" _, err = gitOpsFromString(t, config) assert.ErrorContains(t, err, "'team_settings.secrets' is required") } else { + // 'software' is not allowed in global config + config := getConfig(nil) + config += "software:\n packages:\n - url: https://example.com\n" + path1, basePath1 := createTempFile(t, "", config) + appConfig := fleet.EnrichedAppConfig{} + appConfig.License = &fleet.LicenseInfo{ + Tier: fleet.TierPremium, + } + _, err = GitOpsFromFile(path1, basePath1, &appConfig, nopLogf) + assert.ErrorContains(t, err, "'software' cannot be set on global file") + // Invalid org_settings - config := getConfig([]string{"org_settings"}) + config = getConfig([]string{"org_settings"}) config += "org_settings:\n path: [2]\n" _, err = gitOpsFromString(t, config) assert.ErrorContains(t, err, "failed to unmarshal org_settings") @@ -595,9 +638,6 @@ func TestTopLevelGitOpsValidation(t *testing.T) { "missing_all": { optsToExclude: []string{"controls", "queries", "policies", "agent_options", "org_settings"}, }, - "missing_controls": { - optsToExclude: []string{"controls"}, - }, "missing_queries": { optsToExclude: []string{"queries"}, }, @@ -724,7 +764,7 @@ func TestGitOpsPaths(t *testing.T) { err = os.WriteFile(mainTmpFile.Name(), []byte(config), 0o644) require.NoError(t, err) - _, err = GitOpsFromFile(mainTmpFile.Name(), dir, nil) + _, err = GitOpsFromFile(mainTmpFile.Name(), dir, nil, nopLogf) assert.NoError(t, err) // Test a bad path @@ -737,7 +777,7 @@ func TestGitOpsPaths(t *testing.T) { err = os.WriteFile(mainTmpFile.Name(), []byte(config), 0o644) require.NoError(t, err) - _, err = GitOpsFromFile(mainTmpFile.Name(), dir, nil) + _, err = GitOpsFromFile(mainTmpFile.Name(), dir, nil, nopLogf) assert.ErrorContains(t, err, "no such file or directory") // Test a bad file -- cannot be unmarshalled @@ -772,7 +812,7 @@ func TestGitOpsPaths(t *testing.T) { } err = os.WriteFile(mainTmpFile.Name(), []byte(config), 0o644) require.NoError(t, err) - _, err = GitOpsFromFile(mainTmpFile.Name(), dir, nil) + _, err = GitOpsFromFile(mainTmpFile.Name(), dir, nil, nopLogf) assert.ErrorContains(t, err, "nested paths are not supported") }, ) @@ -830,7 +870,7 @@ software: Tier: fleet.TierPremium, } path, basePath := createTempFile(t, "", config) - _, err = GitOpsFromFile(path, basePath, &appConfig) + _, err = GitOpsFromFile(path, basePath, &appConfig, nopLogf) assert.ErrorContains(t, err, fmt.Sprintf("software URL \"%s\" is too long, must be less than 256 characters", tooBigURL)) // Policy references a software installer not present in the team. @@ -857,7 +897,7 @@ software: 0o755, ) require.NoError(t, err) - _, err = GitOpsFromFile(path, basePath, &appConfig) + _, err = GitOpsFromFile(path, basePath, &appConfig, nopLogf) assert.ErrorContains(t, err, "install_software.package_path URL https://statics.teams.cdn.office.net/production-osx/enterprise/webview2/lkg/MicrosoftTeams.pkg not found on team", ) @@ -889,7 +929,7 @@ software: appConfig.License = &fleet.LicenseInfo{ Tier: fleet.TierPremium, } - _, err = GitOpsFromFile(path, basePath, &appConfig) + _, err = GitOpsFromFile(path, basePath, &appConfig, nopLogf) assert.ErrorContains(t, err, "failed to unmarshal install_software.package_path file") } diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index 73f19ead9c01..0b3a0e498362 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -2974,7 +2974,7 @@ func (ds *Datastore) ListPoliciesForHost(ctx context.Context, host *fleet.Host) FROM policies p LEFT JOIN policy_membership pm ON (p.id=pm.policy_id AND host_id=?) LEFT JOIN users u ON p.author_id = u.id - WHERE (p.team_id IS NULL OR p.team_id = (select team_id from hosts WHERE id = ?)) + WHERE (p.team_id IS NULL OR p.team_id = COALESCE((SELECT team_id FROM hosts WHERE id = ?), 0)) AND (p.platforms IS NULL OR p.platforms = '' OR FIND_IN_SET(?, p.platforms) != 0) ORDER BY FIELD(response, 'fail', '', 'pass'), p.name` diff --git a/server/datastore/mysql/migrations/tables/20240905200001_AddPoliciesToNoTeam.go b/server/datastore/mysql/migrations/tables/20240905200001_AddPoliciesToNoTeam.go new file mode 100644 index 000000000000..d24592b9f0cd --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20240905200001_AddPoliciesToNoTeam.go @@ -0,0 +1,78 @@ +package tables + +import ( + "database/sql" + "fmt" + + "github.com/pkg/errors" +) + +func init() { + MigrationClient.AddMigration(Up_20240905200001, Down_20240905200001) +} + +func Up_20240905200001(tx *sql.Tx) error { + // + // Changes in `policies` and `policy_stats` to support policies for "No team". + // "No team" here means policies that run on hosts that belong to no team (hosts.team_id = NULL) + // + // `policies`: + // - team_id = NULL means the policy is a "Global policy" (aka "All teams" policy). + // - team_id > 0 means the policy is a team policy. + // - team_id = 0 means the policy is a "No team" policy. + // + // `policy_stats`: + // - For "Global policies": + // - inherited_team_id_char = 'global', inherited_team_id = NULL are the stats for the policy's global domain. + // - inherited_team_id_char = '', inherited_team_id = are the stats of the policy on a specific team domain. + // - inherited_team_id_car = '0', inherited_team_id = 0 are the stats of the policy on the "No team" domain. + // - For "Team policies" (for team policies there's always just one row in this table): + // - inherited_team_id_char = 'global', inherited_team_id = NULL are the stats for the team policy. + // + + // Drop foreign key on policies table to teams to allow for team_id = 0 to represent "No team". + referencedTables := map[string]struct{}{"teams": {}} + table := "policies" + constraints, err := constraintsForTable(tx, table, referencedTables) + if err != nil { + return err + } + if len(constraints) != 1 { + return errors.New("policies foreign key to teams not found") + } + if _, err := tx.Exec(fmt.Sprintf(` + ALTER TABLE policies + DROP FOREIGN KEY %s; + `, constraints[0])); err != nil { + return fmt.Errorf("failed to drop policies foreign key to teams: %w", err) + } + + // Allow `inherited_team_id` to be NULL to represent global policy stats on the global domain, and `inherited_team_id = 0` + // to represent global policy stats on the "No team" domain. + // Add `inherited_team_id_char` as generated column to add uniqueness constraint to the table for policies on each domain. + if _, err := tx.Exec(` + ALTER TABLE policy_stats + DROP INDEX policy_team_unique, + MODIFY inherited_team_id INT UNSIGNED NULL, + ADD COLUMN inherited_team_id_char char(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + GENERATED ALWAYS AS (IF(inherited_team_id IS NULL, 'global', CONVERT(inherited_team_id, CHAR))), + ADD UNIQUE KEY (policy_id, inherited_team_id_char); + `); err != nil { + return fmt.Errorf("failed to modify inherited_team_id in policy_stats: %w", err) + } + + // Update inherited_team_id from `0` to `NULL` to allow storing stats for the "No team" domain as `inherited_team_id = 0`. + if _, err := tx.Exec(` + UPDATE policy_stats + SET inherited_team_id = NULL + WHERE inherited_team_id = 0; + `); err != nil { + return fmt.Errorf("failed to update policy_stats: %w", err) + } + + return nil +} + +func Down_20240905200001(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/migrations/tables/20240905200001_AddPoliciesToNoTeam_test.go b/server/datastore/mysql/migrations/tables/20240905200001_AddPoliciesToNoTeam_test.go new file mode 100644 index 000000000000..90a2495ea548 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20240905200001_AddPoliciesToNoTeam_test.go @@ -0,0 +1,86 @@ +package tables + +import ( + "strconv" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestUp_20240905200001(t *testing.T) { + db := applyUpToPrev(t) + + team1ID := uint(execNoErrLastID(t, db, `INSERT INTO teams (name) VALUES ('team1');`)) + globalPolicy0 := uint(execNoErrLastID(t, db, + `INSERT INTO policies (name, query, description, checksum) VALUES + ('globalPolicy0', 'SELECT 0', 'Description', 'checksum');`, + )) + policy1Team1 := uint(execNoErrLastID(t, db, + `INSERT INTO policies (name, query, description, team_id, checksum) + VALUES ('policy1Team1', 'SELECT 1', 'Description', ?, 'checksum2');`, + team1ID, + )) + + // Insert policy stats for a global policy. + execNoErr(t, db, + `INSERT INTO policy_stats + (policy_id, inherited_team_id, passing_host_count, failing_host_count) + VALUES + (?, ?, 1, 2), (?, ?, 3, 4);`, + globalPolicy0, + 0, + globalPolicy0, + policy1Team1, + ) + // Insert policy stats for a team policy. + execNoErr(t, db, + `INSERT INTO policy_stats (policy_id, inherited_team_id, passing_host_count, failing_host_count) + VALUES (?, ?, 5, 6);`, + policy1Team1, + 0, + ) + + applyNext(t, db) + + // Check the policy_stats for global have been migrated correctly. + var results []struct { + PolicyID uint `db:"policy_id"` + InheritedTeamID *uint `db:"inherited_team_id"` + InheritedTeamIDChar string `db:"inherited_team_id_char"` + PassingHostCount uint `db:"passing_host_count"` + FailingHostCount uint `db:"failing_host_count"` + } + err := db.Select(&results, + `SELECT policy_id, inherited_team_id, inherited_team_id_char, passing_host_count, failing_host_count + FROM policy_stats ORDER BY policy_id ASC;`, + ) + require.NoError(t, err) + require.Len(t, results, 3) + + require.Equal(t, globalPolicy0, results[0].PolicyID) + require.Nil(t, results[0].InheritedTeamID) + require.Equal(t, "global", results[0].InheritedTeamIDChar) + require.Equal(t, uint(1), results[0].PassingHostCount) + require.Equal(t, uint(2), results[0].FailingHostCount) + + require.Equal(t, globalPolicy0, results[1].PolicyID) + require.NotNil(t, results[1].InheritedTeamID) + require.Equal(t, policy1Team1, *results[1].InheritedTeamID) + require.Equal(t, strconv.FormatUint(uint64(policy1Team1), 10), results[1].InheritedTeamIDChar) + require.Equal(t, uint(3), results[1].PassingHostCount) + require.Equal(t, uint(4), results[1].FailingHostCount) + + require.Equal(t, policy1Team1, results[2].PolicyID) + require.Nil(t, results[2].InheritedTeamID) + require.Equal(t, "global", results[2].InheritedTeamIDChar) + require.Equal(t, uint(5), results[2].PassingHostCount) + require.Equal(t, uint(6), results[2].FailingHostCount) + + // The team can be deleted, and the policy won't be automatically deleted. + execNoErr(t, db, + `DELETE FROM teams;`, + ) + var ok bool + err = db.Get(&ok, `SELECT 1 FROM policies WHERE id = ?;`, policy1Team1) + require.NoError(t, err) +} diff --git a/server/datastore/mysql/policies.go b/server/datastore/mysql/policies.go index 1c3cc024114c..f96f99289d7f 100644 --- a/server/datastore/mysql/policies.go +++ b/server/datastore/mysql/policies.go @@ -12,9 +12,9 @@ import ( "golang.org/x/text/unicode/norm" - "github.com/doug-martin/goqu/v9" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/ptr" kitlog "github.com/go-kit/log" "github.com/go-kit/log/level" "github.com/jmoiron/sqlx" @@ -103,8 +103,7 @@ func policyDB(ctx context.Context, q sqlx.QueryerContext, id uint, teamID *uint) FROM policies p LEFT JOIN users u ON p.author_id = u.id LEFT JOIN policy_stats ps ON p.id = ps.policy_id - AND ((p.team_id IS NULL AND ps.inherited_team_id = 0) - OR (p.team_id IS NOT NULL AND ps.inherited_team_id = p.team_id)) + AND ((p.team_id IS NULL AND ps.inherited_team_id IS NULL) OR (p.team_id IS NOT NULL)) WHERE p.id=? AND %s`, policyCols, teamWhere), args...) if err != nil { @@ -381,7 +380,7 @@ func listPoliciesDB(ctx context.Context, q sqlx.QueryerContext, teamID *uint, op COALESCE(ps.failing_host_count, 0) AS failing_host_count FROM policies p LEFT JOIN users u ON p.author_id = u.id - LEFT JOIN policy_stats ps ON p.id = ps.policy_id AND ps.inherited_team_id = 0 + LEFT JOIN policy_stats ps ON p.id = ps.policy_id AND ps.inherited_team_id IS NULL ` if teamID != nil { @@ -498,8 +497,7 @@ func (ds *Datastore) PoliciesByID(ctx context.Context, ids []uint) (map[uint]*fl FROM policies p LEFT JOIN users u ON p.author_id = u.id LEFT JOIN policy_stats ps ON p.id = ps.policy_id - AND ((p.team_id IS NULL AND ps.inherited_team_id = 0) - OR (p.team_id IS NOT NULL AND ps.inherited_team_id = p.team_id)) + AND ((p.team_id IS NULL AND ps.inherited_team_id IS NULL) OR (p.team_id IS NOT NULL)) WHERE p.id IN (?)` query, args, err := sqlx.In(sql, ids) if err != nil { @@ -556,38 +554,25 @@ func deletePolicyDB(ctx context.Context, q sqlx.ExtContext, ids []uint, teamID * // PolicyQueriesForHost returns the policy queries that are to be executed on the given host. func (ds *Datastore) PolicyQueriesForHost(ctx context.Context, host *fleet.Host) (map[string]string, error) { - var rows []struct { - ID string `db:"id"` - Query string `db:"query"` - } if host.FleetPlatform() == "" { // We log to help troubleshooting in case this happens, as the host // won't be receiving any policies targeted for specific platforms. level.Error(ds.logger).Log("err", "unrecognized platform", "hostID", host.ID, "platform", host.Platform) //nolint:errcheck } - q := dialect.From("policies").Select( - goqu.I("id"), - goqu.I("query"), - ).Where( - goqu.And( - goqu.Or( - goqu.I("platforms").Eq(""), - goqu.L("FIND_IN_SET(?, ?)", - host.FleetPlatform(), - goqu.I("platforms"), - ).Neq(0), - ), - goqu.Or( - goqu.I("team_id").IsNull(), // global policies - goqu.I("team_id").Eq(host.TeamID), // team policies - ), - ), - ) - sql, args, err := q.ToSQL() - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "selecting policies sql build") + const stmt = ` + SELECT id, query + FROM policies + WHERE + -- team_id == NULL are global policies that apply to all hosts + -- team_id == 0 are policies that apply to hosts in "No team" + -- team_id > 0 are policies that apply to hosts in teams + (team_id IS NULL OR team_id = COALESCE(?, 0)) AND + (platforms = '' OR FIND_IN_SET(?, platforms))` + var rows []struct { + ID string `db:"id"` + Query string `db:"query"` } - if err := sqlx.SelectContext(ctx, ds.reader(ctx), &rows, sql, args...); err != nil { + if err := sqlx.SelectContext(ctx, ds.reader(ctx), &rows, stmt, host.TeamID, host.FleetPlatform()); err != nil { return nil, ctxerr.Wrap(ctx, err, "selecting policies for host") } results := make(map[string]string) @@ -607,6 +592,18 @@ func (ds *Datastore) NewTeamPolicy(ctx context.Context, teamID uint, authorID *u args.Query = q.Query args.Description = q.Description } + // Check team exists. + if teamID > 0 { + var ok bool + err := ds.writer(ctx).GetContext(ctx, &ok, `SELECT COUNT(*) = 1 FROM teams WHERE id = ?`, teamID) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "get team id") + } + if !ok { + return nil, ctxerr.Wrap(ctx, notFound("Team").WithID(teamID), "get team id") + } + + } // We must normalize the name for full Unicode support (Unicode equivalence). nameUnicode := norm.NFC.String(args.Name) res, err := ds.writer(ctx).ExecContext(ctx, @@ -659,7 +656,7 @@ func (ds *Datastore) ListMergedTeamPolicies(ctx context.Context, teamID uint, op FROM policies p LEFT JOIN users u ON p.author_id = u.id LEFT JOIN policy_stats ps ON p.id = ps.policy_id - AND ps.inherited_team_id = IF(p.team_id IS NULL, ?, 0) + AND (p.team_id IS NOT NULL OR ps.inherited_team_id = ?) WHERE (p.team_id = ? OR p.team_id IS NULL) ` @@ -699,9 +696,9 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs queryerContext := ds.writer(ctx) // Preprocess specs and group them by team - teamNameToID := make(map[string]uint, 1) - teamIDToPolicies := make(map[uint][]*fleet.PolicySpec, 1) - softwareInstallerIDs := make(map[uint]map[uint]*uint) // teamID -> titleID -> softwareInstallerID + teamNameToID := make(map[string]*uint, 1) + teamIDToPolicies := make(map[*uint][]*fleet.PolicySpec, 1) + softwareInstallerIDs := make(map[*uint]map[uint]*uint) // teamID -> titleID -> softwareInstallerID // Get the team IDs for _, spec := range specs { @@ -711,13 +708,18 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs teamID, ok := teamNameToID[spec.Team] if !ok { if spec.Team != "" { - // if team name is not empty, it must have a team ID; otherwise teamID defaults to 0 value - err := sqlx.GetContext(ctx, queryerContext, &teamID, `SELECT id FROM teams WHERE name = ?`, spec.Team) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return ctxerr.Wrap(ctx, notFound("Team").WithName(spec.Team), "get team id") + if spec.Team == "No team" { + teamID = ptr.Uint(0) + } else { + var tmID uint + err := sqlx.GetContext(ctx, queryerContext, &tmID, `SELECT id FROM teams WHERE name = ?`, spec.Team) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return ctxerr.Wrap(ctx, notFound("Team").WithName(spec.Team), "get team id") + } + return ctxerr.Wrap(ctx, err, "get team id") } - return ctxerr.Wrap(ctx, err, "get team id") + teamID = &tmID } } teamNameToID[spec.Team] = teamID @@ -755,7 +757,7 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs Query string `db:"query"` Platforms string `db:"platforms"` } - teamIDToPoliciesByName := make(map[uint]map[string]policyLite, len(teamIDToPolicies)) + teamIDToPoliciesByName := make(map[*uint]map[string]policyLite, len(teamIDToPolicies)) for teamID, teamPolicySpecs := range teamIDToPolicies { teamIDToPoliciesByName[teamID] = make(map[string]policyLite, len(teamPolicySpecs)) policyNames := make([]string, 0, len(teamPolicySpecs)) @@ -766,11 +768,11 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs var query string var args []interface{} var err error - if teamID == 0 { + if teamID == nil { query, args, err = sqlx.In("SELECT name, query, platforms FROM policies WHERE team_id IS NULL AND name IN (?)", policyNames) } else { query, args, err = sqlx.In( - "SELECT name, query, platforms FROM policies WHERE team_id = ? AND name IN (?)", &teamID, policyNames, + "SELECT name, query, platforms FROM policies WHERE team_id = ? AND name IN (?)", *teamID, policyNames, ) } if err != nil { @@ -814,10 +816,6 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs `, policiesChecksumComputedColumn(), ) for teamID, teamPolicySpecs := range teamIDToPolicies { - var teamIDPtr *uint - if teamID != 0 { - teamIDPtr = &teamID - } for _, spec := range teamPolicySpecs { var softwareInstallerID *uint if spec.SoftwareTitleID != nil { @@ -825,7 +823,7 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs } res, err := tx.ExecContext( ctx, - query, spec.Name, spec.Query, spec.Description, authorID, spec.Resolution, teamIDPtr, spec.Platform, spec.Critical, + query, spec.Name, spec.Query, spec.Description, authorID, spec.Resolution, teamID, spec.Platform, spec.Critical, spec.CalendarEventsEnabled, softwareInstallerID, ) if err != nil { @@ -1408,7 +1406,7 @@ func (ds *Datastore) UpdateHostPolicyCounts(ctx context.Context) error { WHERE p.team_id IS NULL AND p.id = ? GROUP BY t.id, p.id` err = sqlx.SelectContext(ctx, db, &policyStats, selectStmt, policy.ID) - if err != nil && !errors.Is(err, sql.ErrNoRows) { + if err != nil { if errors.Is(err, sql.ErrNoRows) { // Policy or team was deleted by a parallel process. We proceed. level.Error(ds.logger).Log( @@ -1418,6 +1416,38 @@ func (ds *Datastore) UpdateHostPolicyCounts(ctx context.Context) error { } return ctxerr.Wrap(ctx, err, "select policy counts for inherited global policies") } + + noTeamStmt := `SELECT + p.id as policy_id, + 0 AS inherited_team_id, -- 0 means "No team" + ( + SELECT COUNT(*) + FROM policy_membership pm + INNER JOIN hosts h ON pm.host_id = h.id + WHERE pm.policy_id = p.id AND pm.passes = true AND h.team_id IS NULL + ) AS passing_host_count, + ( + SELECT COUNT(*) + FROM policy_membership pm + INNER JOIN hosts h ON pm.host_id = h.id + WHERE pm.policy_id = p.id AND pm.passes = false AND h.team_id IS NULL + ) AS failing_host_count + FROM policies p + WHERE p.team_id IS NULL AND p.id = ?` + var noTeamPolicyStats []policyStat + err = sqlx.SelectContext(ctx, db, &noTeamPolicyStats, noTeamStmt, policy.ID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + // Policy was deleted by a parallel process. We proceed. + level.Error(ds.logger).Log( + "msg", "'No team' policy not found for inherited global policies. Was policy deleted?", "policy_id", policy.ID, + ) + continue + } + return ctxerr.Wrap(ctx, err, "select policy counts for inherited global policies for 'no team' policies") + } + policyStats = append(policyStats, noTeamPolicyStats...) + insertStmt := `INSERT INTO policy_stats (policy_id, inherited_team_id, passing_host_count, failing_host_count) VALUES (:policy_id, :inherited_team_id, :passing_host_count, :failing_host_count) ON DUPLICATE KEY UPDATE @@ -1441,7 +1471,7 @@ func (ds *Datastore) UpdateHostPolicyCounts(ctx context.Context) error { INSERT INTO policy_stats (policy_id, inherited_team_id, passing_host_count, failing_host_count) SELECT p.id, - 0 AS inherited_team_id, -- using 0 to represent global scope + NULL AS inherited_team_id, -- using NULL to represent global scope COALESCE(SUM(IF(pm.passes IS NULL, 0, pm.passes = 1)), 0), COALESCE(SUM(IF(pm.passes IS NULL, 0, pm.passes = 0)), 0) FROM policies p diff --git a/server/datastore/mysql/policies_test.go b/server/datastore/mysql/policies_test.go index aee58797ff64..c800eeee1c58 100644 --- a/server/datastore/mysql/policies_test.go +++ b/server/datastore/mysql/policies_test.go @@ -67,6 +67,7 @@ func TestPolicies(t *testing.T) { {"TestPoliciesNewGlobalPolicyWithInstaller", testNewGlobalPolicyWithInstaller}, {"TestPoliciesTeamPoliciesWithInstaller", testTeamPoliciesWithInstaller}, {"ApplyPolicySpecWithInstallers", testApplyPolicySpecWithInstallers}, + {"TeamPoliciesNoTeam", testTeamPoliciesNoTeam}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { @@ -1413,6 +1414,14 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) { Team: "team1", Platform: "windows,linux", }, + { + Name: "query4", + Query: "select 4;", + Description: "query4 desc", + Resolution: "some other good resolution 2", + Team: "No team", + Platform: "", + }, })) policies, err := ds.ListGlobalPolicies(ctx, fleet.ListOptions{}) @@ -1450,6 +1459,21 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) { assert.Equal(t, "windows,linux", teamPolicies[1].Platform) assert.False(t, teamPolicies[1].CalendarEventsEnabled) + noTeamPolicies, _, err := ds.ListTeamPolicies(ctx, fleet.PolicyNoTeamID, fleet.ListOptions{}, fleet.ListOptions{}) + require.NoError(t, err) + require.Len(t, noTeamPolicies, 1) + assert.Equal(t, "query4", noTeamPolicies[0].Name) + assert.Equal(t, "select 4;", noTeamPolicies[0].Query) + assert.Equal(t, "query4 desc", noTeamPolicies[0].Description) + require.NotNil(t, noTeamPolicies[0].AuthorID) + assert.Equal(t, user1.ID, *noTeamPolicies[0].AuthorID) + require.NotNil(t, noTeamPolicies[0].Resolution) + assert.Equal(t, "some other good resolution 2", *noTeamPolicies[0].Resolution) + assert.Equal(t, "", noTeamPolicies[0].Platform) + assert.False(t, noTeamPolicies[0].CalendarEventsEnabled) + assert.NotNil(t, noTeamPolicies[0].TeamID) + assert.Zero(t, *noTeamPolicies[0].TeamID) + // Make sure apply is idempotent require.NoError(t, ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{ { @@ -1477,6 +1501,14 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) { Team: "team1", Platform: "windows,linux", }, + { + Name: "query4", + Query: "select 4;", + Description: "query4 desc", + Resolution: "some other good resolution 2", + Team: "No team", + Platform: "", + }, })) policies, err = ds.ListGlobalPolicies(ctx, fleet.ListOptions{}) @@ -1485,6 +1517,9 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) { teamPolicies, _, err = ds.ListTeamPolicies(ctx, team1.ID, fleet.ListOptions{}, fleet.ListOptions{}) require.NoError(t, err) require.Len(t, teamPolicies, 2) + noTeamPolicies, _, err = ds.ListTeamPolicies(ctx, fleet.PolicyNoTeamID, fleet.ListOptions{}, fleet.ListOptions{}) + require.NoError(t, err) + require.Len(t, noTeamPolicies, 1) // Test policy updating. require.NoError(t, ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{ @@ -3964,7 +3999,19 @@ func testTeamPoliciesWithInstaller(t *testing.T, ds *Datastore) { require.NotNil(t, p2.SoftwareInstallerID) require.Equal(t, installerID, *p2.SoftwareInstallerID) - policiesWithInstallers, err := ds.GetPoliciesWithAssociatedInstaller(ctx, team1.ID, []uint{}) + // Policy p4 in "No team" with associated installer. + p4, err := ds.NewTeamPolicy(ctx, fleet.PolicyNoTeamID, &user1.ID, fleet.PolicyPayload{ + Name: "p4", + Query: "SELECT 4;", + SoftwareInstallerID: ptr.Uint(installerID), + }) + require.NoError(t, err) + policiesWithInstallers, err := ds.GetPoliciesWithAssociatedInstaller(ctx, fleet.PolicyNoTeamID, []uint{p4.ID}) + require.NoError(t, err) + require.Len(t, policiesWithInstallers, 1) + require.Equal(t, p4.ID, policiesWithInstallers[0].ID) + + policiesWithInstallers, err = ds.GetPoliciesWithAssociatedInstaller(ctx, team1.ID, []uint{}) require.NoError(t, err) require.Empty(t, policiesWithInstallers) @@ -4014,6 +4061,7 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) { require.NoError(t, err) team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team2"}) require.NoError(t, err) + installer1ID, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ InstallScript: "hello", PreInstallQuery: "SELECT 1;", @@ -4048,6 +4096,23 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) { installer2, err := ds.GetSoftwareInstallerMetadataByID(ctx, installer2ID) require.NoError(t, err) require.NotNil(t, installer2.TitleID) + installer3ID, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ + InstallScript: "hello3", + PreInstallQuery: "SELECT 3;", + PostInstallScript: "world3", + InstallerFile: bytes.NewReader([]byte("hello3")), + StorageID: "storage3", + Filename: "file3", + Title: "file3", + Version: "1.0", + Source: "rpm_packages", + UserID: user1.ID, + TeamID: nil, + }) + require.NoError(t, err) + installer3, err := ds.GetSoftwareInstallerMetadataByID(ctx, installer3ID) + require.NoError(t, err) + require.NotNil(t, installer3.TitleID) // Installers cannot be assigned to global policies. err = ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{ @@ -4064,7 +4129,7 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) { require.Error(t, err) require.ErrorIs(t, err, errSoftwareTitleIDOnGlobalPolicy) - // Apply two team policies associated to two installers. + // Apply two team policies associated to two installers and a "No team" policy associated to an installer. err = ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{ { Name: "Team policy 1", @@ -4084,6 +4149,15 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) { Platform: "linux", SoftwareTitleID: installer2.TitleID, }, + { + Name: "No team policy 3", + Query: "SELECT 3;", + Description: "Description 3", + Resolution: "Resolution 3", + Team: "No team", + Platform: "linux", + SoftwareTitleID: installer3.TitleID, + }, }) require.NoError(t, err) team1Policies, _, err := ds.ListTeamPolicies(ctx, team1.ID, fleet.ListOptions{}, fleet.ListOptions{}) @@ -4096,6 +4170,11 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) { require.Len(t, team2Policies, 1) require.NotNil(t, team2Policies[0].SoftwareInstallerID) require.Equal(t, installer2.InstallerID, *team2Policies[0].SoftwareInstallerID) + noTeamPolicies, _, err := ds.ListTeamPolicies(ctx, fleet.PolicyNoTeamID, fleet.ListOptions{}, fleet.ListOptions{}) + require.NoError(t, err) + require.Len(t, noTeamPolicies, 1) + require.NotNil(t, noTeamPolicies[0].SoftwareInstallerID) + require.Equal(t, installer3.InstallerID, *noTeamPolicies[0].SoftwareInstallerID) // Unset software installer from "Team policy 1". err = ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{ @@ -4115,7 +4194,7 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) { require.Len(t, team1Policies, 1) require.Nil(t, team1Policies[0].SoftwareInstallerID) - // Set software installer "Team policy 1" to a software installer on team2. + // Set "Team policy 1" to a software installer on team2. err = ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{ { Name: "Team policy 1", @@ -4131,7 +4210,22 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) { var notFoundErr *notFoundError require.ErrorAs(t, err, ¬FoundErr) - // Set software installer "Team policy 1" to a software title that doesn't exist. + // Set "No team policy 3" to a software installer on team2. + err = ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{ + { + Name: "No team policy 3", + Query: "SELECT 3;", + Description: "Description 3", + Resolution: "Resolution 3", + Team: "No team", + Platform: "darwin", + SoftwareTitleID: installer2.TitleID, + }, + }) + require.Error(t, err) + require.ErrorAs(t, err, ¬FoundErr) + + // Set "Team policy 1" to a software title that doesn't exist. err = ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{ { Name: "Team policy 1", @@ -4146,6 +4240,21 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) { require.Error(t, err) require.ErrorAs(t, err, ¬FoundErr) + // Set "No team policy 3" to a software title that doesn't exist. + err = ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{ + { + Name: "No team policy 3", + Query: "SELECT 3;", + Description: "Description 3", + Resolution: "Resolution 3", + Team: "No team", + Platform: "darwin", + SoftwareTitleID: ptr.Uint(999_999), + }, + }) + require.Error(t, err) + require.ErrorAs(t, err, ¬FoundErr) + // Unset software installer from "Team policy 2" using 0. err = ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{ { @@ -4165,7 +4274,7 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) { require.Nil(t, team2Policies[0].SoftwareInstallerID) // Apply team policies associated to two installers (again, with two installers with the same title). - installer3ID, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ + installer4ID, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ InstallScript: "hello3", PreInstallQuery: "SELECT 3;", PostInstallScript: "world3", @@ -4179,7 +4288,7 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) { TeamID: &team2.ID, }) require.NoError(t, err) - installer3, err := ds.GetSoftwareInstallerMetadataByID(ctx, installer3ID) + installer4, err := ds.GetSoftwareInstallerMetadataByID(ctx, installer4ID) require.NoError(t, err) require.NotNil(t, installer2.TitleID) err = ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{ @@ -4199,7 +4308,7 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) { Resolution: "Resolution 2", Team: "team2", Platform: "linux", - SoftwareTitleID: installer3.TitleID, + SoftwareTitleID: installer4.TitleID, }, }) require.NoError(t, err) @@ -4212,5 +4321,392 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) { require.NoError(t, err) require.Len(t, team2Policies, 1) require.NotNil(t, team2Policies[0].SoftwareInstallerID) - require.Equal(t, installer3.InstallerID, *team2Policies[0].SoftwareInstallerID) + require.Equal(t, installer4.InstallerID, *team2Policies[0].SoftwareInstallerID) +} + +func testTeamPoliciesNoTeam(t *testing.T, ds *Datastore) { + ctx := context.Background() + + user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true) + team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"}) + require.NoError(t, err) + team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team2"}) + require.NoError(t, err) + + newHost := func(name string, teamID *uint, platform string) *fleet.Host { + h, err := ds.NewHost(ctx, &fleet.Host{ + OsqueryHostID: ptr.String(uuid.New().String()), + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + NodeKey: ptr.String(uuid.New().String()), + UUID: uuid.New().String(), + Hostname: name, + TeamID: teamID, + Platform: platform, + }) + require.NoError(t, err) + return h + } + + host0NoTeam := newHost("host0NoTeam", nil, "darwin") + host1Team1 := newHost("host1Team1", &team1.ID, "darwin") + host2Team1 := newHost("host2Team1", &team1.ID, "linux") + host3Team2 := newHost("host1Team1", &team2.ID, "windows") + host5NoTeam := newHost("host5NoTeam", nil, "windows") + + policy0NoTeam, err := ds.NewTeamPolicy(ctx, fleet.PolicyNoTeamID, &user1.ID, fleet.PolicyPayload{ + Name: "policy0NoTeam", + Query: "SELECT 0;", + }) + require.NoError(t, err) + require.NotNil(t, policy0NoTeam.TeamID) + require.Equal(t, fleet.PolicyNoTeamID, *policy0NoTeam.TeamID) + tp, err := ds.TeamPolicy(ctx, fleet.PolicyNoTeamID, policy0NoTeam.ID) + require.NoError(t, err) + require.Equal(t, tp, policy0NoTeam) + + policy1Team1, err := ds.NewTeamPolicy(ctx, team1.ID, &user1.ID, fleet.PolicyPayload{ + Name: "policy1Team1", + Query: "SELECT 1;", + }) + require.NoError(t, err) + policy2Team2, err := ds.NewTeamPolicy(ctx, team2.ID, &user1.ID, fleet.PolicyPayload{ + Name: "policy2Team2", + Query: "SELECT 2;", + }) + require.NoError(t, err) + policy3NoTeam, err := ds.NewTeamPolicy(ctx, fleet.PolicyNoTeamID, &user1.ID, fleet.PolicyPayload{ + Name: "policy3NoTeam", + Query: "SELECT 3;", + }) + require.NoError(t, err) + policy4Team2, err := ds.NewTeamPolicy(ctx, team2.ID, &user1.ID, fleet.PolicyPayload{ + Name: "policy4Team2", + Query: "SELECT 4;", + }) + require.NoError(t, err) + + globalPolicy1, err := ds.NewGlobalPolicy(ctx, &user1.ID, fleet.PolicyPayload{ + Name: "globalPolicy1", + Query: "SELECT gp1;", + }) + require.NoError(t, err) + globalPolicy2, err := ds.NewGlobalPolicy(ctx, &user1.ID, fleet.PolicyPayload{ + Name: "globalPolicy2", + Query: "SELECT gp2;", + }) + require.NoError(t, err) + + // Results for host0NoTeam + err = ds.RecordPolicyQueryExecutions(ctx, host0NoTeam, map[uint]*bool{ + globalPolicy1.ID: ptr.Bool(false), + globalPolicy2.ID: ptr.Bool(false), + policy0NoTeam.ID: ptr.Bool(true), + policy3NoTeam.ID: ptr.Bool(false), + }, time.Now(), false) + require.NoError(t, err) + + // Results for host1Team1 + err = ds.RecordPolicyQueryExecutions(ctx, host1Team1, map[uint]*bool{ + globalPolicy1.ID: ptr.Bool(true), + globalPolicy2.ID: nil, // failed to execute, e.g. typo on SQL. + policy1Team1.ID: ptr.Bool(true), + }, time.Now(), false) + require.NoError(t, err) + + // Results for host2Team1 + err = ds.RecordPolicyQueryExecutions(ctx, host2Team1, map[uint]*bool{ + globalPolicy1.ID: ptr.Bool(false), + globalPolicy2.ID: ptr.Bool(true), + policy1Team1.ID: ptr.Bool(false), + }, time.Now(), false) + require.NoError(t, err) + + // Results for host3Team2 + err = ds.RecordPolicyQueryExecutions(ctx, host3Team2, map[uint]*bool{ + globalPolicy1.ID: ptr.Bool(true), + policy2Team2.ID: ptr.Bool(true), + policy4Team2.ID: ptr.Bool(false), + }, time.Now(), false) + require.NoError(t, err) + + // Results for host5NoTeam + err = ds.RecordPolicyQueryExecutions(ctx, host5NoTeam, map[uint]*bool{ + globalPolicy1.ID: ptr.Bool(true), + globalPolicy2.ID: ptr.Bool(false), + policy0NoTeam.ID: ptr.Bool(false), + policy3NoTeam.ID: ptr.Bool(false), + }, time.Now(), false) + require.NoError(t, err) + + err = ds.UpdateHostPolicyCounts(ctx) + require.NoError(t, err) + + // Tests on global domain. + globalPolicies, err := ds.ListGlobalPolicies(ctx, fleet.ListOptions{}) + require.NoError(t, err) + require.Len(t, globalPolicies, 2) + require.Equal(t, globalPolicy1.ID, globalPolicies[0].ID) + require.Equal(t, uint(2), globalPolicies[0].FailingHostCount) + require.Equal(t, uint(3), globalPolicies[0].PassingHostCount) + require.Equal(t, globalPolicy2.ID, globalPolicies[1].ID) + require.Equal(t, uint(2), globalPolicies[1].FailingHostCount) + require.Equal(t, uint(1), globalPolicies[1].PassingHostCount) + ids := make([]uint, 0, len(globalPolicies)) + for _, globalPolicy := range globalPolicies { + p, err := ds.Policy(ctx, globalPolicy.ID) + require.NoError(t, err) + require.Equal(t, p, globalPolicy) + ids = append(ids, globalPolicy.ID) + } + c, err := ds.CountPolicies(ctx, nil, "") + require.NoError(t, err) + require.Equal(t, 2, c) + globalPoliciesByID, err := ds.PoliciesByID(ctx, ids) + require.NoError(t, err) + require.Len(t, globalPoliciesByID, 2) + require.Equal(t, globalPoliciesByID[globalPolicies[0].ID], globalPolicies[0]) + require.Equal(t, globalPoliciesByID[globalPolicies[1].ID], globalPolicies[1]) + + // Tests on team1 domain. + teamPolicies, inheritedPolicies, err := ds.ListTeamPolicies(ctx, team1.ID, fleet.ListOptions{}, fleet.ListOptions{}) + require.NoError(t, err) + require.Len(t, teamPolicies, 1) + require.Equal(t, policy1Team1.ID, teamPolicies[0].ID) + require.Equal(t, uint(1), teamPolicies[0].FailingHostCount) + require.Equal(t, uint(1), teamPolicies[0].PassingHostCount) + require.Len(t, inheritedPolicies, 2) + require.Equal(t, globalPolicy1.ID, inheritedPolicies[0].ID) + require.Equal(t, uint(1), inheritedPolicies[0].FailingHostCount) + require.Equal(t, uint(1), inheritedPolicies[0].PassingHostCount) + require.Equal(t, globalPolicy2.ID, inheritedPolicies[1].ID) + require.Equal(t, uint(0), inheritedPolicies[1].FailingHostCount) + require.Equal(t, uint(1), inheritedPolicies[1].PassingHostCount) + ids = make([]uint, 0, len(teamPolicies)) + for _, teamPolicy := range teamPolicies { + p, err := ds.Policy(ctx, teamPolicy.ID) + require.NoError(t, err) + require.Equal(t, p, teamPolicy) + ids = append(ids, teamPolicy.ID) + } + teamPoliciesByID, err := ds.PoliciesByID(ctx, ids) + require.NoError(t, err) + require.Len(t, teamPoliciesByID, 1) + require.Equal(t, teamPoliciesByID[teamPolicies[0].ID], teamPolicies[0]) + c, err = ds.CountMergedTeamPolicies(ctx, team1.ID, "") + require.NoError(t, err) + require.Equal(t, 3, c) + c, err = ds.CountPolicies(ctx, &team1.ID, "") + require.NoError(t, err) + require.Equal(t, 1, c) + mergedTeamPolicies, err := ds.ListMergedTeamPolicies(ctx, team1.ID, fleet.ListOptions{}) + require.NoError(t, err) + require.Len(t, mergedTeamPolicies, 3) + require.Equal(t, policy1Team1.ID, mergedTeamPolicies[0].ID) + require.Equal(t, uint(1), mergedTeamPolicies[0].FailingHostCount) + require.Equal(t, uint(1), mergedTeamPolicies[0].PassingHostCount) + require.Equal(t, globalPolicy1.ID, mergedTeamPolicies[1].ID) + require.Equal(t, uint(1), mergedTeamPolicies[1].FailingHostCount) + require.Equal(t, uint(1), mergedTeamPolicies[1].PassingHostCount) + require.Equal(t, globalPolicy2.ID, mergedTeamPolicies[2].ID) + require.Equal(t, uint(0), mergedTeamPolicies[2].FailingHostCount) + require.Equal(t, uint(1), mergedTeamPolicies[2].PassingHostCount) + + // Tests on team2 domain. + teamPolicies, inheritedPolicies, err = ds.ListTeamPolicies(ctx, team2.ID, fleet.ListOptions{}, fleet.ListOptions{}) + require.NoError(t, err) + require.Len(t, teamPolicies, 2) + require.Equal(t, policy2Team2.ID, teamPolicies[0].ID) + require.Equal(t, uint(0), teamPolicies[0].FailingHostCount) + require.Equal(t, uint(1), teamPolicies[0].PassingHostCount) + require.Equal(t, policy4Team2.ID, teamPolicies[1].ID) + require.Equal(t, uint(1), teamPolicies[1].FailingHostCount) + require.Equal(t, uint(0), teamPolicies[1].PassingHostCount) + require.Len(t, inheritedPolicies, 2) + require.Equal(t, globalPolicy1.ID, inheritedPolicies[0].ID) + require.Equal(t, uint(0), inheritedPolicies[0].FailingHostCount) + require.Equal(t, uint(1), inheritedPolicies[0].PassingHostCount) + require.Equal(t, globalPolicy2.ID, inheritedPolicies[1].ID) + require.Equal(t, uint(0), inheritedPolicies[1].FailingHostCount) + require.Equal(t, uint(0), inheritedPolicies[1].PassingHostCount) + ids = make([]uint, 0, len(teamPolicies)) + for _, teamPolicy := range teamPolicies { + p, err := ds.Policy(ctx, teamPolicy.ID) + require.NoError(t, err) + require.Equal(t, p, teamPolicy) + ids = append(ids, teamPolicy.ID) + } + teamPoliciesByID, err = ds.PoliciesByID(ctx, ids) + require.NoError(t, err) + require.Len(t, teamPoliciesByID, 2) + require.Equal(t, teamPoliciesByID[teamPolicies[0].ID], teamPolicies[0]) + require.Equal(t, teamPoliciesByID[teamPolicies[1].ID], teamPolicies[1]) + c, err = ds.CountMergedTeamPolicies(ctx, team2.ID, "") + require.NoError(t, err) + require.Equal(t, 4, c) + c, err = ds.CountPolicies(ctx, &team2.ID, "") + require.NoError(t, err) + require.Equal(t, 2, c) + mergedTeamPolicies, err = ds.ListMergedTeamPolicies(ctx, team2.ID, fleet.ListOptions{}) + require.NoError(t, err) + require.Len(t, mergedTeamPolicies, 4) + require.Equal(t, policy2Team2.ID, mergedTeamPolicies[0].ID) + require.Equal(t, uint(0), mergedTeamPolicies[0].FailingHostCount) + require.Equal(t, uint(1), mergedTeamPolicies[0].PassingHostCount) + require.Equal(t, policy4Team2.ID, mergedTeamPolicies[1].ID) + require.Equal(t, uint(1), mergedTeamPolicies[1].FailingHostCount) + require.Equal(t, uint(0), mergedTeamPolicies[1].PassingHostCount) + require.Equal(t, globalPolicy1.ID, mergedTeamPolicies[2].ID) + require.Equal(t, uint(0), mergedTeamPolicies[2].FailingHostCount) + require.Equal(t, uint(1), mergedTeamPolicies[2].PassingHostCount) + require.Equal(t, globalPolicy2.ID, mergedTeamPolicies[3].ID) + require.Equal(t, uint(0), mergedTeamPolicies[3].FailingHostCount) + require.Equal(t, uint(0), mergedTeamPolicies[3].PassingHostCount) + + // Tests on "No team" domain. + teamPolicies, inheritedPolicies, err = ds.ListTeamPolicies(ctx, fleet.PolicyNoTeamID, fleet.ListOptions{}, fleet.ListOptions{}) + require.NoError(t, err) + require.Len(t, teamPolicies, 2) + require.Equal(t, policy0NoTeam.ID, teamPolicies[0].ID) + require.Equal(t, uint(1), teamPolicies[0].FailingHostCount) + require.Equal(t, uint(1), teamPolicies[0].PassingHostCount) + require.Equal(t, policy3NoTeam.ID, teamPolicies[1].ID) + require.Equal(t, uint(2), teamPolicies[1].FailingHostCount) + require.Equal(t, uint(0), teamPolicies[1].PassingHostCount) + require.Len(t, inheritedPolicies, 2) + require.Equal(t, globalPolicy1.ID, inheritedPolicies[0].ID) + require.Equal(t, uint(1), inheritedPolicies[0].FailingHostCount) + require.Equal(t, uint(1), inheritedPolicies[0].PassingHostCount) + require.Equal(t, globalPolicy2.ID, inheritedPolicies[1].ID) + require.Equal(t, uint(2), inheritedPolicies[1].FailingHostCount) + require.Equal(t, uint(0), inheritedPolicies[1].PassingHostCount) + ids = make([]uint, 0, len(teamPolicies)) + for _, teamPolicy := range teamPolicies { + p, err := ds.Policy(ctx, teamPolicy.ID) + require.NoError(t, err) + require.Equal(t, p, teamPolicy) + ids = append(ids, teamPolicy.ID) + } + teamPoliciesByID, err = ds.PoliciesByID(ctx, ids) + require.NoError(t, err) + require.Len(t, teamPoliciesByID, 2) + require.Equal(t, teamPoliciesByID[teamPolicies[0].ID], teamPolicies[0]) + require.Equal(t, teamPoliciesByID[teamPolicies[1].ID], teamPolicies[1]) + c, err = ds.CountMergedTeamPolicies(ctx, fleet.PolicyNoTeamID, "") + require.NoError(t, err) + require.Equal(t, 4, c) + c, err = ds.CountPolicies(ctx, ptr.Uint(fleet.PolicyNoTeamID), "") + require.NoError(t, err) + require.Equal(t, 2, c) + mergedTeamPolicies, err = ds.ListMergedTeamPolicies(ctx, fleet.PolicyNoTeamID, fleet.ListOptions{}) + require.NoError(t, err) + require.Len(t, mergedTeamPolicies, 4) + require.Equal(t, policy0NoTeam.ID, mergedTeamPolicies[0].ID) + require.Equal(t, uint(1), mergedTeamPolicies[0].FailingHostCount) + require.Equal(t, uint(1), mergedTeamPolicies[0].PassingHostCount) + require.Equal(t, policy3NoTeam.ID, mergedTeamPolicies[1].ID) + require.Equal(t, uint(2), mergedTeamPolicies[1].FailingHostCount) + require.Equal(t, uint(0), mergedTeamPolicies[1].PassingHostCount) + require.Equal(t, globalPolicy1.ID, mergedTeamPolicies[2].ID) + require.Equal(t, uint(1), mergedTeamPolicies[2].FailingHostCount) + require.Equal(t, uint(1), mergedTeamPolicies[2].PassingHostCount) + require.Equal(t, globalPolicy2.ID, mergedTeamPolicies[3].ID) + require.Equal(t, uint(2), mergedTeamPolicies[3].FailingHostCount) + require.Equal(t, uint(0), mergedTeamPolicies[3].PassingHostCount) + + // Test ListPoliciesForHost and PolicyQueriesForHost for host0NoTeam. + host0Policies, err := ds.ListPoliciesForHost(ctx, host0NoTeam) + require.NoError(t, err) + require.Len(t, host0Policies, 4) + require.Equal(t, globalPolicy1.ID, host0Policies[0].ID) + require.Equal(t, "fail", host0Policies[0].Response) + require.Equal(t, globalPolicy2.ID, host0Policies[1].ID) + require.Equal(t, "fail", host0Policies[1].Response) + require.Equal(t, policy3NoTeam.ID, host0Policies[2].ID) + require.Equal(t, "fail", host0Policies[2].Response) + require.Equal(t, policy0NoTeam.ID, host0Policies[3].ID) + require.Equal(t, "pass", host0Policies[3].Response) + host0PolicyQueries, err := ds.PolicyQueriesForHost(ctx, host0NoTeam) + require.NoError(t, err) + require.Len(t, host0PolicyQueries, 4) + require.Equal(t, "SELECT gp1;", host0PolicyQueries[strconv.FormatUint(uint64(globalPolicy1.ID), 10)]) + require.Equal(t, "SELECT gp2;", host0PolicyQueries[strconv.FormatUint(uint64(globalPolicy2.ID), 10)]) + require.Equal(t, "SELECT 0;", host0PolicyQueries[strconv.FormatUint(uint64(policy0NoTeam.ID), 10)]) + require.Equal(t, "SELECT 3;", host0PolicyQueries[strconv.FormatUint(uint64(policy3NoTeam.ID), 10)]) + + // Test ListPoliciesForHost and PolicyQueriesForHost for host1Team1. + host1Policies, err := ds.ListPoliciesForHost(ctx, host1Team1) + require.NoError(t, err) + require.Len(t, host1Policies, 3) + require.Equal(t, globalPolicy2.ID, host1Policies[0].ID) + require.Equal(t, "", host1Policies[0].Response) + require.Equal(t, globalPolicy1.ID, host1Policies[1].ID) + require.Equal(t, "pass", host1Policies[1].Response) + require.Equal(t, policy1Team1.ID, host1Policies[2].ID) + require.Equal(t, "pass", host1Policies[2].Response) + host1PolicyQueries, err := ds.PolicyQueriesForHost(ctx, host1Team1) + require.NoError(t, err) + require.Len(t, host1PolicyQueries, 3) + require.Equal(t, "SELECT gp1;", host1PolicyQueries[strconv.FormatUint(uint64(globalPolicy1.ID), 10)]) + require.Equal(t, "SELECT gp2;", host1PolicyQueries[strconv.FormatUint(uint64(globalPolicy2.ID), 10)]) + require.Equal(t, "SELECT 1;", host1PolicyQueries[strconv.FormatUint(uint64(policy1Team1.ID), 10)]) + + // Test ListPoliciesForHost and PolicyQueriesForHost for host2Team1. + host2Policies, err := ds.ListPoliciesForHost(ctx, host2Team1) + require.NoError(t, err) + require.Len(t, host2Policies, 3) + require.Equal(t, globalPolicy1.ID, host2Policies[0].ID) + require.Equal(t, "fail", host2Policies[0].Response) + require.Equal(t, policy1Team1.ID, host2Policies[1].ID) + require.Equal(t, "fail", host2Policies[1].Response) + require.Equal(t, globalPolicy2.ID, host2Policies[2].ID) + require.Equal(t, "pass", host2Policies[2].Response) + host2PolicyQueries, err := ds.PolicyQueriesForHost(ctx, host2Team1) + require.NoError(t, err) + require.Len(t, host2PolicyQueries, 3) + require.Equal(t, "SELECT gp1;", host2PolicyQueries[strconv.FormatUint(uint64(globalPolicy1.ID), 10)]) + require.Equal(t, "SELECT gp2;", host2PolicyQueries[strconv.FormatUint(uint64(globalPolicy2.ID), 10)]) + require.Equal(t, "SELECT 1;", host2PolicyQueries[strconv.FormatUint(uint64(policy1Team1.ID), 10)]) + + // Test ListPoliciesForHost and PolicyQueriesForHost for host3Team2. + host3Policies, err := ds.ListPoliciesForHost(ctx, host3Team2) + require.NoError(t, err) + require.Len(t, host3Policies, 4) + require.Equal(t, policy4Team2.ID, host3Policies[0].ID) + require.Equal(t, "fail", host3Policies[0].Response) + require.Equal(t, globalPolicy2.ID, host3Policies[1].ID) + require.Equal(t, "", host3Policies[1].Response) + require.Equal(t, globalPolicy1.ID, host3Policies[2].ID) + require.Equal(t, "pass", host3Policies[2].Response) + require.Equal(t, policy2Team2.ID, host3Policies[3].ID) + require.Equal(t, "pass", host3Policies[3].Response) + host3PolicyQueries, err := ds.PolicyQueriesForHost(ctx, host3Team2) + require.NoError(t, err) + require.Len(t, host3PolicyQueries, 4) + require.Equal(t, "SELECT gp1;", host3PolicyQueries[strconv.FormatUint(uint64(globalPolicy1.ID), 10)]) + require.Equal(t, "SELECT gp2;", host3PolicyQueries[strconv.FormatUint(uint64(globalPolicy2.ID), 10)]) + require.Equal(t, "SELECT 2;", host3PolicyQueries[strconv.FormatUint(uint64(policy2Team2.ID), 10)]) + require.Equal(t, "SELECT 4;", host3PolicyQueries[strconv.FormatUint(uint64(policy4Team2.ID), 10)]) + + // Test ListPoliciesForHost and PolicyQueriesForHost for host5NoTeam. + host5Policies, err := ds.ListPoliciesForHost(ctx, host5NoTeam) + require.NoError(t, err) + require.Len(t, host5Policies, 4) + require.Equal(t, globalPolicy2.ID, host5Policies[0].ID) + require.Equal(t, "fail", host5Policies[0].Response) + require.Equal(t, policy0NoTeam.ID, host5Policies[1].ID) + require.Equal(t, "fail", host5Policies[1].Response) + require.Equal(t, policy3NoTeam.ID, host5Policies[2].ID) + require.Equal(t, "fail", host5Policies[2].Response) + require.Equal(t, globalPolicy1.ID, host5Policies[3].ID) + require.Equal(t, "pass", host5Policies[3].Response) + host5PolicyQueries, err := ds.PolicyQueriesForHost(ctx, host5NoTeam) + require.NoError(t, err) + require.Len(t, host5PolicyQueries, 4) + require.Equal(t, "SELECT gp1;", host5PolicyQueries[strconv.FormatUint(uint64(globalPolicy1.ID), 10)]) + require.Equal(t, "SELECT gp2;", host5PolicyQueries[strconv.FormatUint(uint64(globalPolicy2.ID), 10)]) + require.Equal(t, "SELECT 0;", host5PolicyQueries[strconv.FormatUint(uint64(policy0NoTeam.ID), 10)]) + require.Equal(t, "SELECT 3;", host5PolicyQueries[strconv.FormatUint(uint64(policy3NoTeam.ID), 10)]) } diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index d30b3f12ef63..290ecaf57754 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -1038,9 +1038,9 @@ CREATE TABLE `migration_status_tables` ( `tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`) -) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=312 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=313 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'); +INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `mobile_device_management_solutions` ( @@ -1394,7 +1394,6 @@ CREATE TABLE `policies` ( KEY `idx_policies_author_id` (`author_id`), KEY `idx_policies_team_id` (`team_id`), KEY `fk_policies_software_installer_id` (`software_installer_id`), - CONSTRAINT `policies_ibfk_2` FOREIGN KEY (`team_id`) REFERENCES `teams` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT `policies_ibfk_3` FOREIGN KEY (`software_installer_id`) REFERENCES `software_installers` (`id`), CONSTRAINT `policies_queries_ibfk_1` FOREIGN KEY (`author_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; @@ -1429,15 +1428,16 @@ CREATE TABLE `policy_membership` ( CREATE TABLE `policy_stats` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `policy_id` int unsigned NOT NULL, - `inherited_team_id` int unsigned NOT NULL DEFAULT '0', + `inherited_team_id` int unsigned DEFAULT NULL, `passing_host_count` mediumint unsigned NOT NULL DEFAULT '0', `failing_host_count` mediumint unsigned NOT NULL DEFAULT '0', `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `inherited_team_id_char` char(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci GENERATED ALWAYS AS (if((`inherited_team_id` is null),_utf8mb4'global',cast(`inherited_team_id` as char charset utf8mb4))) VIRTUAL, PRIMARY KEY (`id`), - UNIQUE KEY `policy_team_unique` (`policy_id`,`inherited_team_id`), + UNIQUE KEY `policy_id` (`policy_id`,`inherited_team_id_char`), CONSTRAINT `policy_stats_ibfk_1` FOREIGN KEY (`policy_id`) REFERENCES `policies` (`id`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; diff --git a/server/datastore/mysql/vpp.go b/server/datastore/mysql/vpp.go index d40b41be8173..1127497f888e 100644 --- a/server/datastore/mysql/vpp.go +++ b/server/datastore/mysql/vpp.go @@ -191,9 +191,12 @@ func (ds *Datastore) SetTeamVPPApps(ctx context.Context, teamID *uint, appFleets } } - vppToken, err := ds.GetVPPTokenByTeamID(ctx, teamID) - if err != nil { - return ctxerr.Wrap(ctx, err, "SetTeamVPPApps retrieve VPP token ID") + var vppToken *fleet.VPPTokenDB + if len(appFleets) > 0 { + vppToken, err = ds.GetVPPTokenByTeamID(ctx, teamID) + if err != nil { + return ctxerr.Wrap(ctx, err, "SetTeamVPPApps retrieve VPP token ID") + } } return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error { @@ -858,7 +861,6 @@ func (ds *Datastore) UpdateVPPTokenTeams(ctx context.Context, id uint, teams []u return nil }) - if err != nil { var mysqlErr *mysql.MySQLError // https://dev.mysql.com/doc/mysql-errors/8.4/en/server-error-reference.html#error_er_dup_entry diff --git a/server/fleet/policies.go b/server/fleet/policies.go index a66ccc00a453..53849a6227b6 100644 --- a/server/fleet/policies.go +++ b/server/fleet/policies.go @@ -77,6 +77,9 @@ var ( errPolicyInvalidPlatform = errors.New("invalid policy platform") ) +// PolicyNoTeamID is the team ID of "No team" policies. +const PolicyNoTeamID = uint(0) + // Verify verifies the policy payload is valid. func (p PolicyPayload) Verify() error { if p.QueryID != nil { diff --git a/server/service/client.go b/server/service/client.go index 9840fb65868e..8d40d6d93990 100644 --- a/server/service/client.go +++ b/server/service/client.go @@ -687,7 +687,7 @@ func (c *Client) ApplyGroup( for tmName, software := range tmSoftwarePackagesPayloads { // For non-dry run, currentTeamName and tmName are the same currentTeamName := getTeamName(tmName) - logfn("[+] applying software installers for team %s\n", tmName) + logfn("[+] applying %d software packages for team %s\n", len(software), tmName) installers, err := c.ApplyTeamSoftwareInstallers(currentTeamName, software, opts.ApplySpecOptions) if err != nil { return nil, nil, fmt.Errorf("applying software installers for team %q: %w", tmName, err) @@ -1283,9 +1283,7 @@ func (c *Client) DoGitOps( } } group.AppConfig.(map[string]interface{})["scripts"] = scripts - - group.Software = config.Software.Packages - } else { + } else if !config.IsNoTeam() { team = make(map[string]interface{}) team["name"] = *config.TeamName team["agent_options"] = config.AgentOptions @@ -1339,111 +1337,115 @@ func (c *Client) DoGitOps( team["mdm"] = map[string]interface{}{} mdmAppConfig = team["mdm"].(map[string]interface{}) } - // Common controls settings between org and team settings - // Put in default values for macos_settings - if config.Controls.MacOSSettings != nil { - mdmAppConfig["macos_settings"] = config.Controls.MacOSSettings - } else { - mdmAppConfig["macos_settings"] = map[string]interface{}{} - } - macOSSettings := mdmAppConfig["macos_settings"].(map[string]interface{}) - if customSettings, ok := macOSSettings["custom_settings"]; !ok || customSettings == nil { - macOSSettings["custom_settings"] = []interface{}{} - } - // Put in default values for macos_updates - if config.Controls.MacOSUpdates != nil { - mdmAppConfig["macos_updates"] = config.Controls.MacOSUpdates - } else { - mdmAppConfig["macos_updates"] = map[string]interface{}{} - } - macOSUpdates := mdmAppConfig["macos_updates"].(map[string]interface{}) - if minimumVersion, ok := macOSUpdates["minimum_version"]; !ok || minimumVersion == nil { - macOSUpdates["minimum_version"] = "" - } - if deadline, ok := macOSUpdates["deadline"]; !ok || deadline == nil { - macOSUpdates["deadline"] = "" - } - // Put in default values for ios_updates - if config.Controls.IOSUpdates != nil { - mdmAppConfig["ios_updates"] = config.Controls.IOSUpdates - } else { - mdmAppConfig["ios_updates"] = map[string]interface{}{} - } - iOSUpdates := mdmAppConfig["ios_updates"].(map[string]interface{}) - if minimumVersion, ok := iOSUpdates["minimum_version"]; !ok || minimumVersion == nil { - iOSUpdates["minimum_version"] = "" - } - if deadline, ok := iOSUpdates["deadline"]; !ok || deadline == nil { - iOSUpdates["deadline"] = "" - } - // Put in default values for ipados_updates - if config.Controls.IPadOSUpdates != nil { - mdmAppConfig["ipados_updates"] = config.Controls.IPadOSUpdates - } else { - mdmAppConfig["ipados_updates"] = map[string]interface{}{} - } - iPadOSUpdates := mdmAppConfig["ipados_updates"].(map[string]interface{}) - if minimumVersion, ok := iPadOSUpdates["minimum_version"]; !ok || minimumVersion == nil { - iPadOSUpdates["minimum_version"] = "" - } - if deadline, ok := iPadOSUpdates["deadline"]; !ok || deadline == nil { - iPadOSUpdates["deadline"] = "" - } - // Put in default values for macos_setup - if config.Controls.MacOSSetup != nil { - mdmAppConfig["macos_setup"] = config.Controls.MacOSSetup - } else { - mdmAppConfig["macos_setup"] = map[string]interface{}{} - } - macOSSetup := mdmAppConfig["macos_setup"].(map[string]interface{}) - if bootstrapPackage, ok := macOSSetup["bootstrap_package"]; !ok || bootstrapPackage == nil { - macOSSetup["bootstrap_package"] = "" - } - if enableEndUserAuthentication, ok := macOSSetup["enable_end_user_authentication"]; !ok || enableEndUserAuthentication == nil { - macOSSetup["enable_end_user_authentication"] = false - } - if macOSSetupAssistant, ok := macOSSetup["macos_setup_assistant"]; !ok || macOSSetupAssistant == nil { - macOSSetup["macos_setup_assistant"] = "" - } - // Put in default values for windows_settings - if config.Controls.WindowsSettings != nil { - mdmAppConfig["windows_settings"] = config.Controls.WindowsSettings - } else { - mdmAppConfig["windows_settings"] = map[string]interface{}{} - } - windowsSettings := mdmAppConfig["windows_settings"].(map[string]interface{}) - if customSettings, ok := windowsSettings["custom_settings"]; !ok || customSettings == nil { - windowsSettings["custom_settings"] = []interface{}{} - } - // Put in default values for windows_updates - if config.Controls.WindowsUpdates != nil { - mdmAppConfig["windows_updates"] = config.Controls.WindowsUpdates - } else { - mdmAppConfig["windows_updates"] = map[string]interface{}{} - } - if appConfig.License.IsPremium() { - windowsUpdates := mdmAppConfig["windows_updates"].(map[string]interface{}) - if deadlineDays, ok := windowsUpdates["deadline_days"]; !ok || deadlineDays == nil { - windowsUpdates["deadline_days"] = nil + + if !config.IsNoTeam() { + // Common controls settings between org and team settings + // Put in default values for macos_settings + if config.Controls.MacOSSettings != nil { + mdmAppConfig["macos_settings"] = config.Controls.MacOSSettings + } else { + mdmAppConfig["macos_settings"] = map[string]interface{}{} } - if gracePeriodDays, ok := windowsUpdates["grace_period_days"]; !ok || gracePeriodDays == nil { - windowsUpdates["grace_period_days"] = nil + macOSSettings := mdmAppConfig["macos_settings"].(map[string]interface{}) + if customSettings, ok := macOSSettings["custom_settings"]; !ok || customSettings == nil { + macOSSettings["custom_settings"] = []interface{}{} } - } - // Put in default value for enable_disk_encryption - if config.Controls.EnableDiskEncryption != nil { - mdmAppConfig["enable_disk_encryption"] = config.Controls.EnableDiskEncryption - } else { - mdmAppConfig["enable_disk_encryption"] = false - } - if config.TeamName != nil { - team["gitops_filename"] = filename - rawTeam, err := json.Marshal(team) - if err != nil { - return nil, fmt.Errorf("error marshalling team spec: %w", err) + // Put in default values for macos_updates + if config.Controls.MacOSUpdates != nil { + mdmAppConfig["macos_updates"] = config.Controls.MacOSUpdates + } else { + mdmAppConfig["macos_updates"] = map[string]interface{}{} + } + macOSUpdates := mdmAppConfig["macos_updates"].(map[string]interface{}) + if minimumVersion, ok := macOSUpdates["minimum_version"]; !ok || minimumVersion == nil { + macOSUpdates["minimum_version"] = "" + } + if deadline, ok := macOSUpdates["deadline"]; !ok || deadline == nil { + macOSUpdates["deadline"] = "" + } + // Put in default values for ios_updates + if config.Controls.IOSUpdates != nil { + mdmAppConfig["ios_updates"] = config.Controls.IOSUpdates + } else { + mdmAppConfig["ios_updates"] = map[string]interface{}{} + } + iOSUpdates := mdmAppConfig["ios_updates"].(map[string]interface{}) + if minimumVersion, ok := iOSUpdates["minimum_version"]; !ok || minimumVersion == nil { + iOSUpdates["minimum_version"] = "" + } + if deadline, ok := iOSUpdates["deadline"]; !ok || deadline == nil { + iOSUpdates["deadline"] = "" + } + // Put in default values for ipados_updates + if config.Controls.IPadOSUpdates != nil { + mdmAppConfig["ipados_updates"] = config.Controls.IPadOSUpdates + } else { + mdmAppConfig["ipados_updates"] = map[string]interface{}{} + } + iPadOSUpdates := mdmAppConfig["ipados_updates"].(map[string]interface{}) + if minimumVersion, ok := iPadOSUpdates["minimum_version"]; !ok || minimumVersion == nil { + iPadOSUpdates["minimum_version"] = "" + } + if deadline, ok := iPadOSUpdates["deadline"]; !ok || deadline == nil { + iPadOSUpdates["deadline"] = "" + } + // Put in default values for macos_setup + if config.Controls.MacOSSetup != nil { + mdmAppConfig["macos_setup"] = config.Controls.MacOSSetup + } else { + mdmAppConfig["macos_setup"] = map[string]interface{}{} + } + macOSSetup := mdmAppConfig["macos_setup"].(map[string]interface{}) + if bootstrapPackage, ok := macOSSetup["bootstrap_package"]; !ok || bootstrapPackage == nil { + macOSSetup["bootstrap_package"] = "" + } + if enableEndUserAuthentication, ok := macOSSetup["enable_end_user_authentication"]; !ok || enableEndUserAuthentication == nil { + macOSSetup["enable_end_user_authentication"] = false + } + if macOSSetupAssistant, ok := macOSSetup["macos_setup_assistant"]; !ok || macOSSetupAssistant == nil { + macOSSetup["macos_setup_assistant"] = "" + } + // Put in default values for windows_settings + if config.Controls.WindowsSettings != nil { + mdmAppConfig["windows_settings"] = config.Controls.WindowsSettings + } else { + mdmAppConfig["windows_settings"] = map[string]interface{}{} + } + windowsSettings := mdmAppConfig["windows_settings"].(map[string]interface{}) + if customSettings, ok := windowsSettings["custom_settings"]; !ok || customSettings == nil { + windowsSettings["custom_settings"] = []interface{}{} + } + // Put in default values for windows_updates + if config.Controls.WindowsUpdates != nil { + mdmAppConfig["windows_updates"] = config.Controls.WindowsUpdates + } else { + mdmAppConfig["windows_updates"] = map[string]interface{}{} + } + if appConfig.License.IsPremium() { + windowsUpdates := mdmAppConfig["windows_updates"].(map[string]interface{}) + if deadlineDays, ok := windowsUpdates["deadline_days"]; !ok || deadlineDays == nil { + windowsUpdates["deadline_days"] = nil + } + if gracePeriodDays, ok := windowsUpdates["grace_period_days"]; !ok || gracePeriodDays == nil { + windowsUpdates["grace_period_days"] = nil + } + } + // Put in default value for enable_disk_encryption + if config.Controls.EnableDiskEncryption != nil { + mdmAppConfig["enable_disk_encryption"] = config.Controls.EnableDiskEncryption + } else { + mdmAppConfig["enable_disk_encryption"] = false + } + + if config.TeamName != nil { + team["gitops_filename"] = filename + rawTeam, err := json.Marshal(team) + if err != nil { + return nil, fmt.Errorf("error marshalling team spec: %w", err) + } + group.Teams = []json.RawMessage{rawTeam} + group.TeamsDryRunAssumptions = teamDryRunAssumptions } - group.Teams = []json.RawMessage{rawTeam} - group.TeamsDryRunAssumptions = teamDryRunAssumptions } // Apply org settings, scripts, enroll secrets, team entities (software, scripts, etc.), and controls. @@ -1456,27 +1458,32 @@ func (c *Client) DoGitOps( if err != nil { return nil, err } + var teamSoftwareInstallers []fleet.SoftwareInstaller if config.TeamName != nil { - if len(teamIDsByName) != 1 { - return nil, fmt.Errorf("expected 1 team spec to be applied, got %d", len(teamIDsByName)) - } - teamID, ok := teamIDsByName[*config.TeamName] - if ok && teamID == 0 { - if dryRun { - logFn("[+] would've added any policies/queries to new team %s\n", *config.TeamName) - return nil, nil + if !config.IsNoTeam() { + if len(teamIDsByName) != 1 { + return nil, fmt.Errorf("expected 1 team spec to be applied, got %d", len(teamIDsByName)) + } + teamID, ok := teamIDsByName[*config.TeamName] + if ok && teamID == 0 { + if dryRun { + logFn("[+] would've added any policies/queries to new team %s\n", *config.TeamName) + return nil, nil + } + return nil, fmt.Errorf("team %s not created", *config.TeamName) } - return nil, fmt.Errorf("team %s not created", *config.TeamName) - } - for _, teamID = range teamIDsByName { - config.TeamID = &teamID + for _, teamID = range teamIDsByName { + config.TeamID = &teamID + } + teamSoftwareInstallers = teamsSoftwareInstallers[*config.TeamName] + } else { + noTeamSoftwareInstallers, err := c.doGitOpsNoTeamSoftware(config, baseDir, appConfig, logFn, dryRun) + if err != nil { + return nil, err + } + teamSoftwareInstallers = noTeamSoftwareInstallers } - teamSoftwareInstallers = teamsSoftwareInstallers[*config.TeamName] - } - - if _, err = c.doGitOpsNoTeamSoftware(group, baseDir, appConfig, logFn, dryRun); err != nil { - return nil, err } err = c.doGitOpsPolicies(config, teamSoftwareInstallers, logFn, dryRun) @@ -1492,11 +1499,11 @@ func (c *Client) DoGitOps( return teamAssumptions, nil } -func (c *Client) doGitOpsNoTeamSoftware(specs spec.Group, baseDir string, appconfig *fleet.EnrichedAppConfig, logFn func(format string, args ...interface{}), dryRun bool) ([]fleet.SoftwareInstaller, error) { +func (c *Client) doGitOpsNoTeamSoftware(config *spec.GitOps, baseDir string, appconfig *fleet.EnrichedAppConfig, logFn func(format string, args ...interface{}), dryRun bool) ([]fleet.SoftwareInstaller, error) { var softwareInstallers []fleet.SoftwareInstaller - if len(specs.Teams) == 0 && appconfig != nil && appconfig.License.IsPremium() { - packages := make([]fleet.SoftwarePackageSpec, 0, len(specs.Software)) - for _, software := range specs.Software { + if config.IsNoTeam() && appconfig != nil && appconfig.License.IsPremium() { + packages := make([]fleet.SoftwarePackageSpec, 0, len(config.Software.Packages)) + for _, software := range config.Software.Packages { if software != nil { packages = append(packages, *software) } @@ -1505,23 +1512,31 @@ func (c *Client) doGitOpsNoTeamSoftware(specs spec.Group, baseDir string, appcon if err != nil { return nil, fmt.Errorf("applying software installers: %w", err) } + logFn("[+] applying %d software packages for 'No team'\n", len(payload)) softwareInstallers, err = c.ApplyNoTeamSoftwareInstallers(payload, fleet.ApplySpecOptions{DryRun: dryRun}) if err != nil { return nil, fmt.Errorf("applying software installers: %w", err) } if dryRun { - logFn("[+] would've applied 'No Team' software installers\n") + logFn("[+] would've applied 'No Team' software packages\n") } else { - logFn("[+] applied 'No Team' software installers\n") + logFn("[+] applied 'No Team' software packages\n") } } return softwareInstallers, nil } func (c *Client) doGitOpsPolicies(config *spec.GitOps, teamSoftwareInstallers []fleet.SoftwareInstaller, logFn func(format string, args ...interface{}), dryRun bool) error { + var teamID *uint // Global policies (nil) + switch { + case config.TeamID != nil: // Team policies + teamID = config.TeamID + case config.IsNoTeam(): // "No team" policies + teamID = ptr.Uint(0) + } // Get software titles of packages for the team. - if config.TeamID != nil { + if teamID != nil { softwareTitleURLs := make(map[string]uint) for _, softwareInstaller := range teamSoftwareInstallers { if softwareInstaller.URL == "" { @@ -1555,7 +1570,7 @@ func (c *Client) doGitOpsPolicies(config *spec.GitOps, teamSoftwareInstallers [] } // Get the ids and names of current policies to figure out which ones to delete - policies, err := c.GetPolicies(config.TeamID) + policies, err := c.GetPolicies(teamID) if err != nil { return fmt.Errorf("error getting current policies: %w", err) } @@ -1595,7 +1610,11 @@ func (c *Client) doGitOpsPolicies(config *spec.GitOps, teamSoftwareInstallers [] } if !found { policiesToDelete = append(policiesToDelete, oldItem.ID) - fmt.Printf("[-] deleting policy %s\n", oldItem.Name) + if !dryRun { + logFn("[-] deleting policy %s\n", oldItem.Name) + } else { + logFn("[-] would've deleted policy %s\n", oldItem.Name) + } } } if len(policiesToDelete) > 0 { @@ -1608,7 +1627,16 @@ func (c *Client) doGitOpsPolicies(config *spec.GitOps, teamSoftwareInstallers [] end = len(policiesToDelete) } totalDeleted += end - i - if err := c.DeletePolicies(config.TeamID, policiesToDelete[i:end]); err != nil { + var teamID *uint + switch { + case config.TeamID != nil: // Team policies + teamID = config.TeamID + case config.IsNoTeam(): // No team policies + teamID = ptr.Uint(fleet.PolicyNoTeamID) + default: // Global policies + teamID = nil + } + if err := c.DeletePolicies(teamID, policiesToDelete[i:end]); err != nil { return fmt.Errorf("error deleting policies: %w", err) } logFn("[-] deleted %d policies\n", totalDeleted) diff --git a/server/service/global_policies.go b/server/service/global_policies.go index ed0ef22013f4..87c1d67152d4 100644 --- a/server/service/global_policies.go +++ b/server/service/global_policies.go @@ -487,7 +487,7 @@ func applyPolicySpecsEndpoint(ctx context.Context, request interface{}, svc flee func (svc *Service) checkPolicySpecAuthorization(ctx context.Context, policies []*fleet.PolicySpec) error { checkGlobalPolicyAuth := false for _, policy := range policies { - if policy.Team != "" { + if policy.Team != "" && policy.Team != "No team" { team, err := svc.ds.TeamByName(ctx, policy.Team) if err != nil { // This is so that the proper HTTP status code is returned diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index d739a0649fd2..1757d006bbcd 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -938,6 +938,141 @@ func (s *integrationEnterpriseTestSuite) TestTeamPolicies() { require.Len(t, ts.Policies, 0) } +func (s *integrationEnterpriseTestSuite) TestNoTeamPolicies() { + t := s.T() + ctx := context.Background() + + // + // Test a global admin can read and write "No team" policies. + // + + // List "No team" policies. + ts := listTeamPoliciesResponse{} + s.DoJSON("GET", "/api/latest/fleet/teams/0/policies", nil, http.StatusOK, &ts) + require.Len(t, ts.Policies, 0) + require.Len(t, ts.InheritedPolicies, 0) + // Create a placeholder global policy. + _, err := s.ds.NewGlobalPolicy(ctx, nil, fleet.PolicyPayload{ + Name: "globalPolicy1", + Query: "SELECT 0;", + }) + require.NoError(t, err) + // Create a "No team" policy. + tpParams := teamPolicyRequest{ + Name: "noTeamPolicy1", + Query: "SELECT 1;", + } + r := teamPolicyResponse{} + s.DoJSON("POST", "/api/latest/fleet/teams/0/policies", tpParams, http.StatusOK, &r) + require.NotNil(t, r.Policy.TeamID) + require.Zero(t, *r.Policy.TeamID) + // Test that we can't create a policy with the same name under "No team" domain. + s.DoJSON("POST", "/api/latest/fleet/teams/0/policies", tpParams, http.StatusConflict, &r) + // Create a second "No team" policy. + tpParams = teamPolicyRequest{ + Name: "noTeamPolicy2", + Query: "SELECT 2;", + } + r = teamPolicyResponse{} + s.DoJSON("POST", "/api/latest/fleet/teams/0/policies", tpParams, http.StatusOK, &r) + require.NotNil(t, r.Policy.TeamID) + require.Zero(t, *r.Policy.TeamID) + // List "No team" policies. + ts = listTeamPoliciesResponse{} + s.DoJSON("GET", "/api/latest/fleet/teams/0/policies", nil, http.StatusOK, &ts) + require.Len(t, ts.Policies, 2) + assert.Equal(t, "noTeamPolicy1", ts.Policies[0].Name) + assert.Equal(t, "SELECT 1;", ts.Policies[0].Query) + require.NotNil(t, ts.Policies[0].TeamID) + require.Zero(t, *ts.Policies[0].TeamID) + assert.Equal(t, "noTeamPolicy2", ts.Policies[1].Name) + assert.Equal(t, "SELECT 2;", ts.Policies[1].Query) + require.NotNil(t, ts.Policies[1].TeamID) + require.Zero(t, *ts.Policies[1].TeamID) + require.Len(t, ts.InheritedPolicies, 1) + assert.Equal(t, "globalPolicy1", ts.InheritedPolicies[0].Name) + assert.Equal(t, "SELECT 0;", ts.InheritedPolicies[0].Query) + assert.Nil(t, ts.InheritedPolicies[0].TeamID) + // Test policy count for "No team" policies. + tc := countTeamPoliciesResponse{} + s.DoJSON("GET", "/api/latest/fleet/teams/0/policies/count", nil, http.StatusOK, &tc) + require.Equal(t, 2, tc.Count) + // Test merge inherited for "No team" policies. + ts = listTeamPoliciesResponse{} + s.DoJSON("GET", "/api/latest/fleet/teams/0/policies", nil, http.StatusOK, &ts, "merge_inherited", "true", "order_key", "team_id", "order_direction", "desc") + require.Len(t, ts.Policies, 3) + require.Nil(t, ts.InheritedPolicies) + assert.Equal(t, "noTeamPolicy1", ts.Policies[0].Name) + assert.Equal(t, "SELECT 1;", ts.Policies[0].Query) + assert.Equal(t, "noTeamPolicy2", ts.Policies[1].Name) + assert.Equal(t, "SELECT 2;", ts.Policies[1].Query) + assert.Equal(t, "globalPolicy1", ts.Policies[2].Name) + assert.Equal(t, "SELECT 0;", ts.Policies[2].Query) + // Test merge inherited count for "No team" policies. + countResp := countTeamPoliciesResponse{} + s.DoJSON("GET", "/api/latest/fleet/teams/0/policies/count", nil, http.StatusOK, &countResp, "merge_inherited", "true") + require.Nil(t, countResp.Err) + require.Equal(t, 3, countResp.Count) + // Test deleting "No team" policies. + deletePolicyParams := deleteTeamPoliciesRequest{ + IDs: []uint{ts.Policies[0].ID}, + } + deletePolicyResp := deleteTeamPoliciesResponse{} + s.DoJSON("POST", "/api/latest/fleet/teams/0/policies/delete", deletePolicyParams, http.StatusOK, &deletePolicyResp) + ts = listTeamPoliciesResponse{} + s.DoJSON("GET", "/api/latest/fleet/teams/0/policies", nil, http.StatusOK, &ts) + require.Len(t, ts.Policies, 1) + assert.Equal(t, "noTeamPolicy2", ts.Policies[0].Name) + assert.Equal(t, "SELECT 2;", ts.Policies[0].Query) + noTeamPolicy2 := ts.Policies[0] + + // + // Test that a team admin is not allowed to access "No team" policies. + // + + team1, err := s.ds.NewTeam(context.Background(), &fleet.Team{ + Name: "team1", + }) + require.NoError(t, err) + oldToken := s.token + t.Cleanup(func() { + s.token = oldToken + }) + password := test.GoodPassword + email := "testteam@user.com" + team1Admin := &fleet.User{ + Name: "test team user", + Email: email, + GlobalRole: nil, + Teams: []fleet.UserTeam{ + { + Team: *team1, + Role: fleet.RoleAdmin, + }, + }, + } + require.NoError(t, team1Admin.SetPassword(password, 10, 10)) + _, err = s.ds.NewUser(context.Background(), team1Admin) + require.NoError(t, err) + + s.token = s.getTestToken(email, password) + + ts = listTeamPoliciesResponse{} + s.DoJSON("GET", "/api/latest/fleet/teams/0/policies", nil, http.StatusForbidden, &ts) + tpParams = teamPolicyRequest{ + Name: "noTeamPolicy1", + Query: "SELECT 1;", + } + r = teamPolicyResponse{} + s.DoJSON("POST", "/api/latest/fleet/teams/0/policies", tpParams, http.StatusForbidden, &r) + tc = countTeamPoliciesResponse{} + s.DoJSON("GET", "/api/latest/fleet/teams/0/policies/count", nil, http.StatusForbidden, &tc) + deletePolicyParams = deleteTeamPoliciesRequest{ + IDs: []uint{noTeamPolicy2.ID}, + } + s.DoJSON("POST", "/api/latest/fleet/teams/0/policies/delete", deletePolicyParams, http.StatusForbidden, &deleteTeamPoliciesResponse{}) +} + func (s *integrationEnterpriseTestSuite) TestTeamQueries() { t := s.T() @@ -13064,14 +13199,13 @@ func (s *integrationEnterpriseTestSuite) TestVPPAppsWithoutMDM() { func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsSoftwareInstallers() { t := s.T() ctx := context.Background() + test.CreateInsertGlobalVPPToken(t, s.ds) team1, err := s.ds.NewTeam(ctx, &fleet.Team{Name: t.Name() + "team1"}) require.NoError(t, err) team2, err := s.ds.NewTeam(ctx, &fleet.Team{Name: t.Name() + "team2"}) require.NoError(t, err) - test.CreateInsertGlobalVPPToken(t, s.ds) - newHost := func(name string, teamID *uint, platform string) *fleet.Host { h, err := s.ds.NewHost(ctx, &fleet.Host{ DetailUpdatedAt: time.Now(), diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 09876a25ffd0..f8b3fb6790d2 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -9966,8 +9966,8 @@ func (s *integrationMDMTestSuite) TestBatchAssociateAppStoreApps() { }) require.NoError(t, err) - // No vpp token set, no association. - s.Do("POST", batchURL, batchAssociateAppStoreAppsRequest{}, http.StatusUnprocessableEntity, "team_name", tmGood.Name) + // No vpp token set, but request is empty so it succeeds (clears VPP apps for the team). + s.Do("POST", batchURL, batchAssociateAppStoreAppsRequest{}, http.StatusNoContent, "team_name", tmGood.Name) // No vpp token set, try association // FIXME diff --git a/server/service/osquery.go b/server/service/osquery.go index ec90de102794..d3fb9c6920fa 100644 --- a/server/service/osquery.go +++ b/server/service/osquery.go @@ -1623,9 +1623,12 @@ func (svc *Service) processSoftwareForNewlyFailingPolicies( // We do not want to queue software installations on vanilla osquery hosts. return nil } + + var policyTeamID uint if hostTeamID == nil { - // TODO(lucas): Support hosts in "No team". - return nil + policyTeamID = fleet.PolicyNoTeamID + } else { + policyTeamID = *hostTeamID } // Filter out results that are not failures (we are only interested on failing policies, @@ -1643,7 +1646,7 @@ func (svc *Service) processSoftwareForNewlyFailingPolicies( } // Get policies with associated installers for the team. - policiesWithInstaller, err := svc.ds.GetPoliciesWithAssociatedInstaller(ctx, *hostTeamID, incomingFailingPoliciesIDs) + policiesWithInstaller, err := svc.ds.GetPoliciesWithAssociatedInstaller(ctx, policyTeamID, incomingFailingPoliciesIDs) if err != nil { return ctxerr.Wrap(ctx, err, "failed to get policies with installer") } diff --git a/server/service/team_policies.go b/server/service/team_policies.go index 8f68ecddf1f9..74c22fe4e553 100644 --- a/server/service/team_policies.go +++ b/server/service/team_policies.go @@ -187,8 +187,10 @@ func (svc *Service) ListTeamPolicies(ctx context.Context, teamID uint, opts flee return nil, nil, err } - if _, err := svc.ds.Team(ctx, teamID); err != nil { - return nil, nil, ctxerr.Wrapf(ctx, err, "loading team %d", teamID) + if teamID > 0 { + if _, err := svc.ds.Team(ctx, teamID); err != nil { + return nil, nil, ctxerr.Wrapf(ctx, err, "loading team %d", teamID) + } } if mergeInherited { @@ -250,8 +252,10 @@ func (svc *Service) CountTeamPolicies(ctx context.Context, teamID uint, matchQue return 0, err } - if _, err := svc.ds.Team(ctx, teamID); err != nil { - return 0, ctxerr.Wrapf(ctx, err, "loading team %d", teamID) + if teamID > 0 { + if _, err := svc.ds.Team(ctx, teamID); err != nil { + return 0, ctxerr.Wrapf(ctx, err, "loading team %d", teamID) + } } if mergeInherited { @@ -341,8 +345,10 @@ func (svc Service) DeleteTeamPolicies(ctx context.Context, teamID uint, ids []ui return nil, err } - if _, err := svc.ds.Team(ctx, teamID); err != nil { - return nil, ctxerr.Wrapf(ctx, err, "loading team %d", teamID) + if teamID > 0 { + if _, err := svc.ds.Team(ctx, teamID); err != nil { + return nil, ctxerr.Wrapf(ctx, err, "loading team %d", teamID) + } } if len(ids) == 0 { @@ -553,10 +559,6 @@ func (svc *Service) deduceSoftwareInstallerIDFromTitleID(ctx context.Context, te }) } - // - // TODO(lucas): Support "No team" (softwareTitle.SoftwarePackage.TeamID == nil). - // - // At this point we assume *softwareTitle.SoftwarePackage.TeamID == *teamID, // because SoftwareTitleByID above receives the teamID. return ptr.Uint(softwareTitle.SoftwarePackage.InstallerID), nil