<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** For #44077 # Details * Adds `historical_data` key to app and team config (and gitops) with `uptime` and `vulnerabilities` subkeys. Keys default to `true`, meaning "collect this data" * Adds `enabled_historical_dataset` and `disabled_historical_dataset` activities when these values are flipped via GitOps or the config APIs The majority of the file changes in here are GitOps test files that need to be updated to have the new config in them. **This PR does _not_ implement using these configs to actually disable data collection or purge data; that will come in a follow-up PR (as well as the front-end)** # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. n/a, unreleased ## Testing - [X] Added/updated automated tests - [ ] QA'd all new/changed functionality manually #### Defaults - [X] Fresh install: `GET /api/v1/fleet/config` returns `features.historical_data.uptime: true` and `features.historical_data.vulnerabilities: true` - [X] Created a new fleet via `POST /api/v1/fleet/teams`, then `GET /api/v1/fleet/fleets/{id}` returns `features.historical_data.uptime: true` and `features.historical_data.vulnerabilities: true` #### Global PATCH (`POST /api/v1/fleet/config`) - [X] PATCHed `{"features": {"historical_data": {"vulnerabilities": false}}}` — `vulnerabilities` flipped to `false`, `uptime` unchanged at `true` - [X] PATCHed `{"features": {"historical_data": {"uptime": false, "vulnerabilities": true}}}` — both values applied as sent - [X] PATCHed `{"features": {"historical_data": {"vulnerabilites": false}}}` (typo in sub-key) — request rejected with 4xx, stored config unchanged #### Fleet PATCH (`PATCH /api/v1/fleet/fleets/{id}`) - [X] PATCHed a fleet with `{"features": {"historical_data": {"uptime": false}}}` — fleet's `uptime` flipped to `false`, `vulnerabilities` unchanged - [X] Subsequent `GET /api/v1/fleet/fleets/{id}` returns the toggled values under `features.historical_data` (storage shape is symmetric with global) - [X] PATCHed a fleet with `{"features": {"enable_host_users": false}}` (a non-`historical_data` features sub-field) — request returned 200 but the fleet's `enable_host_users` is unchanged (silently ignored, per existing endpoint convention) #### GitOps — global (`fleetctl gitops -f global.yml`) - [X] Applied a YAML with `features.historical_data: {uptime: true, vulnerabilities: false}` — `vulnerabilities` is `false` after apply, `uptime` is `true` - [X] Applied a YAML whose `org_settings` omits `features` entirely — both sub-keys are `true` after apply (defaults injected even if previously disabled) - [X] Applied a YAML where `historical_data` only contains `uptime: false` — `uptime: false` is honored, `vulnerabilities` defaults to `true` - [X] Disabled `vulnerabilities` via the API, then ran `fleetctl gitops` with a YAML that doesn't pin it — `vulnerabilities` flips back to `true` (this is intentional; gitops is the source of truth) #### GitOps — fleet - [X] Applied a fleet YAML with `features.historical_data: {uptime: false}` — that fleet has `uptime: false`, `vulnerabilities: true` after apply - [X] Applied a fleet YAML whose `team_settings.features` omits `historical_data` — both sub-keys are `true` after apply - [X] Applied a fleet YAML that omits `features` entirely — both sub-keys are `true` after apply #### `fleetctl apply` (legacy, partial-merge) - [ ] Disabled `vulnerabilities` via the API, then ran `fleetctl apply` with a YAML that doesn't mention `historical_data` — `vulnerabilities` is still `false` (apply leaves omitted fields alone) #### Activities — global - [X] After PATCHing global to disable `vulnerabilities`, the latest activity is `disabled_historical_dataset` with payload `{"dataset": "vulnerabilities", "fleet_id": null, "fleet_name": null}` - [X] After PATCHing global with both sub-keys flipping in one request, two activities are emitted (one per sub-key) - [X] After PATCHing global with the same values that are already stored, zero new activities are emitted - [X] After re-enabling a previously disabled dataset, the activity type is `enabled_historical_dataset` #### Activities — per fleet - [X] After PATCHing fleet `workstations` to disable `uptime`, the activity is `disabled_historical_dataset` with payload `{"dataset": "uptime", "fleet_id": <workstations id>, "fleet_name": "workstations"}` - [X] Toggling the same dataset on two different fleets produces two distinct activities, one per fleet - [X] After a fleet PATCH with the same values already stored, zero new activities are emitted For unreleased bug fixes in a release candidate, one of: - [X] Confirmed that the fix is not expected to adversely impact load test results - [ ] Alerted the release DRI if additional load testing is needed ## New Fleet configuration settings - [ ] Setting(s) is/are explicitly excluded from GitOps If you didn't check the box above, follow this checklist for GitOps-enabled settings: - [X] Verified that the setting is exported via `fleetctl generate-gitops` - [X] Verified the setting is documented in a separate PR to [the GitOps documentation](https://github.com/fleetdm/fleet/blob/main/docs/Configuration/yaml-files.md#L485) - https://github.com/fleetdm/fleet/pull/44703 - [X] Verified that the setting is cleared on the server if it is not supplied in a YAML file (or that it is documented as being optional) - [X] Verified that any relevant UI is disabled when GitOps mode is enabled <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Historical-data controls: per-org and per-team toggles for uptime and vulnerability time‑series, with defaults applied when keys are omitted and enable/disable activities emitted on changes. * **Bug Fixes** * Partial updates and PATCH/GitOps flows preserve unspecified historical-data sub-keys instead of clearing them. * **Tests** * Expanded unit and integration tests covering defaults, partial PATCH/GitOps behavior, idempotency, and activity emission. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
509 lines
18 KiB
Go
509 lines
18 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
activity_api "github.com/fleetdm/fleet/v4/server/activity/api"
|
|
authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/mock"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestTeamAuth(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
|
|
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true})
|
|
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{}, nil
|
|
}
|
|
ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
|
return &fleet.Team{}, nil
|
|
}
|
|
ds.TeamLiteFunc = func(ctx context.Context, tid uint) (*fleet.TeamLite, error) {
|
|
return &fleet.TeamLite{}, nil
|
|
}
|
|
ds.TeamWithExtrasFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) {
|
|
return &fleet.Team{}, nil
|
|
}
|
|
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
|
return &fleet.Team{}, nil
|
|
}
|
|
ds.ListUsersFunc = func(ctx context.Context, opt fleet.UserListOptions) ([]*fleet.User, error) {
|
|
return nil, nil
|
|
}
|
|
ds.ListTeamsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.ListOptions) ([]*fleet.Team, error) {
|
|
return nil, nil
|
|
}
|
|
ds.DeleteTeamFunc = func(ctx context.Context, tid uint) error {
|
|
return nil
|
|
}
|
|
ds.TeamEnrollSecretsFunc = func(ctx context.Context, teamID uint) ([]*fleet.EnrollSecret, error) {
|
|
return nil, nil
|
|
}
|
|
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
|
|
return nil
|
|
}
|
|
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string,
|
|
) (updates fleet.MDMProfilesUpdates, err error) {
|
|
return fleet.MDMProfilesUpdates{}, nil
|
|
}
|
|
ds.ListHostsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.HostListOptions) ([]*fleet.Host, error) {
|
|
return []*fleet.Host{}, nil
|
|
}
|
|
ds.CleanupDiskEncryptionKeysOnTeamChangeFunc = func(ctx context.Context, hostIDs []uint, newTeamID *uint) error {
|
|
return nil
|
|
}
|
|
|
|
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
|
|
switch name {
|
|
case "team1":
|
|
return &fleet.Team{ID: 1}, nil
|
|
default:
|
|
return &fleet.Team{ID: 2}, nil
|
|
}
|
|
}
|
|
ds.TeamConflictsWithNameFunc = func(ctx context.Context, name string, excludeID uint) (*fleet.Team, error) {
|
|
return nil, nil
|
|
}
|
|
ds.ConditionalAccessMicrosoftGetFunc = func(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error) {
|
|
return nil, ¬FoundError{}
|
|
}
|
|
|
|
ds.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, opts fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
|
|
return []*fleet.CertificateTemplateResponseSummary{}, nil, nil
|
|
}
|
|
|
|
ds.SetHostCertificateTemplatesToPendingRemoveFunc = func(ctx context.Context, teamID uint) error {
|
|
return nil
|
|
}
|
|
|
|
ds.GetEnrollSecretsFunc = func(ctx context.Context, teamID *uint) ([]*fleet.EnrollSecret, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
user *fleet.User
|
|
shouldFailTeamWrite bool
|
|
shouldFailGlobalWrite bool
|
|
shouldFailRead bool
|
|
shouldFailTeamSecretsWrite bool
|
|
}{
|
|
{
|
|
name: "global admin",
|
|
user: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
|
|
shouldFailTeamWrite: false,
|
|
shouldFailGlobalWrite: false,
|
|
shouldFailRead: false,
|
|
shouldFailTeamSecretsWrite: false,
|
|
},
|
|
{
|
|
name: "global maintainer",
|
|
user: &fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
|
|
shouldFailTeamWrite: true,
|
|
shouldFailGlobalWrite: true,
|
|
shouldFailRead: false,
|
|
shouldFailTeamSecretsWrite: false,
|
|
},
|
|
{
|
|
name: "global observer",
|
|
user: &fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
|
|
shouldFailTeamWrite: true,
|
|
shouldFailGlobalWrite: true,
|
|
shouldFailRead: false,
|
|
shouldFailTeamSecretsWrite: true,
|
|
},
|
|
{
|
|
name: "team admin, belongs to team",
|
|
user: &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
|
|
shouldFailTeamWrite: false,
|
|
shouldFailGlobalWrite: true,
|
|
shouldFailRead: false,
|
|
shouldFailTeamSecretsWrite: false,
|
|
},
|
|
{
|
|
name: "team maintainer, belongs to team",
|
|
user: &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
|
|
shouldFailTeamWrite: true,
|
|
shouldFailGlobalWrite: true,
|
|
shouldFailRead: false,
|
|
shouldFailTeamSecretsWrite: false,
|
|
},
|
|
{
|
|
name: "team observer, belongs to team",
|
|
user: &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
|
|
shouldFailTeamWrite: true,
|
|
shouldFailGlobalWrite: true,
|
|
shouldFailRead: false,
|
|
shouldFailTeamSecretsWrite: true,
|
|
},
|
|
{
|
|
name: "team admin, DOES NOT belong to team",
|
|
user: &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleAdmin}}},
|
|
shouldFailTeamWrite: true,
|
|
shouldFailGlobalWrite: true,
|
|
shouldFailRead: true,
|
|
shouldFailTeamSecretsWrite: true,
|
|
},
|
|
{
|
|
name: "team maintainer, DOES NOT belong to team",
|
|
user: &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleMaintainer}}},
|
|
shouldFailTeamWrite: true,
|
|
shouldFailGlobalWrite: true,
|
|
shouldFailRead: true,
|
|
shouldFailTeamSecretsWrite: true,
|
|
},
|
|
{
|
|
name: "team observer, DOES NOT belong to team",
|
|
user: &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleObserver}}},
|
|
shouldFailTeamWrite: true,
|
|
shouldFailGlobalWrite: true,
|
|
shouldFailRead: true,
|
|
shouldFailTeamSecretsWrite: true,
|
|
},
|
|
}
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctx = viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
|
|
|
|
_, err := svc.NewTeam(ctx, fleet.TeamPayload{Name: ptr.String("name")})
|
|
checkAuthErr(t, tt.shouldFailGlobalWrite, err)
|
|
|
|
_, err = svc.ModifyTeam(ctx, 1, fleet.TeamPayload{Name: ptr.String("othername")})
|
|
checkAuthErr(t, tt.shouldFailTeamWrite, err)
|
|
|
|
_, err = svc.ModifyTeamAgentOptions(ctx, 1, nil, fleet.ApplySpecOptions{})
|
|
checkAuthErr(t, tt.shouldFailTeamWrite, err)
|
|
|
|
_, err = svc.AddTeamUsers(ctx, 1, []fleet.TeamUser{})
|
|
checkAuthErr(t, tt.shouldFailTeamWrite, err)
|
|
|
|
_, err = svc.DeleteTeamUsers(ctx, 1, []fleet.TeamUser{})
|
|
checkAuthErr(t, tt.shouldFailTeamWrite, err)
|
|
|
|
_, err = svc.ListTeamUsers(ctx, 1, fleet.ListOptions{})
|
|
checkAuthErr(t, tt.shouldFailRead, err)
|
|
|
|
_, err = svc.ListTeams(ctx, fleet.ListOptions{})
|
|
checkAuthErr(t, false, err) // everybody can do this
|
|
|
|
_, err = svc.GetTeam(ctx, 1)
|
|
checkAuthErr(t, tt.shouldFailRead, err)
|
|
|
|
err = svc.DeleteTeam(ctx, 1)
|
|
checkAuthErr(t, tt.shouldFailTeamWrite, err)
|
|
|
|
_, err = svc.TeamEnrollSecrets(ctx, 1)
|
|
checkAuthErr(t, tt.shouldFailRead, err)
|
|
|
|
_, err = svc.ModifyTeamEnrollSecrets(ctx, 1, []fleet.EnrollSecret{{Secret: "newteamsecret", CreatedAt: time.Now()}})
|
|
checkAuthErr(t, tt.shouldFailTeamSecretsWrite, err)
|
|
|
|
_, err = svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{{Name: "team1"}}, fleet.ApplyTeamSpecOptions{})
|
|
checkAuthErr(t, tt.shouldFailTeamWrite, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestApplyTeamSpecs(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
|
|
opts := &TestServerOpts{License: license, SkipCreateTestUsers: true}
|
|
svc, ctx := newTestService(t, ds, nil, nil, opts)
|
|
user := &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}
|
|
ctx = viewer.NewContext(ctx, viewer.Viewer{User: user})
|
|
ds.TeamConflictsWithNameFunc = func(ctx context.Context, name string, excludeID uint) (*fleet.Team, error) {
|
|
return nil, nil
|
|
}
|
|
baseFeatures := fleet.Features{
|
|
EnableHostUsers: true,
|
|
EnableSoftwareInventory: true,
|
|
AdditionalQueries: ptr.RawMessage(json.RawMessage(`{"foo": "bar"}`)),
|
|
// Hydrated to match what teams loaded from the DB look like after
|
|
// migration 20260423161823 backfills historical_data. Without these
|
|
// defaults, the "Features for existing teams" cases below would
|
|
// diff against the team-spec-applied defaults inside
|
|
// editTeamFromSpec and spuriously emit historical_data enable
|
|
// activities — which the test's activity callback doesn't expect.
|
|
HistoricalData: fleet.HistoricalDataSettings{
|
|
Uptime: true,
|
|
Vulnerabilities: true,
|
|
},
|
|
}
|
|
|
|
mkspec := func(s string) *json.RawMessage {
|
|
return ptr.RawMessage(json.RawMessage(s))
|
|
}
|
|
|
|
t.Run("Features for new teams", func(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
spec *json.RawMessage
|
|
global fleet.Features
|
|
result fleet.Features
|
|
}{
|
|
{
|
|
name: "no spec features uses global config as defaults",
|
|
spec: nil,
|
|
global: baseFeatures,
|
|
result: baseFeatures,
|
|
},
|
|
{
|
|
name: "missing spec features uses new config default values",
|
|
spec: mkspec(`{"enable_software_inventory": false}`),
|
|
global: baseFeatures,
|
|
result: fleet.Features{
|
|
EnableHostUsers: true,
|
|
EnableSoftwareInventory: false,
|
|
AdditionalQueries: nil,
|
|
HistoricalData: fleet.HistoricalDataSettings{Uptime: true, Vulnerabilities: true},
|
|
},
|
|
},
|
|
{
|
|
name: "defaults can be overwritten",
|
|
spec: mkspec(`{"enable_host_users": false}`),
|
|
global: baseFeatures,
|
|
result: fleet.Features{
|
|
EnableHostUsers: false,
|
|
EnableSoftwareInventory: true,
|
|
AdditionalQueries: nil,
|
|
HistoricalData: fleet.HistoricalDataSettings{Uptime: true, Vulnerabilities: true},
|
|
},
|
|
},
|
|
{
|
|
name: "all config can be changed",
|
|
spec: mkspec(`{
|
|
"enable_host_users": false,
|
|
"enable_software_inventory": false,
|
|
"additional_queries": {"example": "query"}
|
|
}`),
|
|
global: baseFeatures,
|
|
result: fleet.Features{
|
|
EnableHostUsers: false,
|
|
EnableSoftwareInventory: false,
|
|
AdditionalQueries: ptr.RawMessage([]byte(`{"example": "query"}`)),
|
|
HistoricalData: fleet.HistoricalDataSettings{Uptime: true, Vulnerabilities: true},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range cases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
|
|
return nil, newNotFoundError()
|
|
}
|
|
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{Features: tt.global}, nil
|
|
}
|
|
|
|
ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
|
require.Equal(t, "team1", team.Name)
|
|
require.Equal(t, tt.result, team.Config.Features)
|
|
team.ID = 1
|
|
return team, nil
|
|
}
|
|
|
|
opts.ActivityMock.NewActivityFunc = func(_ context.Context, _ *activity_api.User, activity activity_api.ActivityDetails) error {
|
|
act := activity.(fleet.ActivityTypeAppliedSpecTeam)
|
|
require.Len(t, act.Teams, 1)
|
|
return nil
|
|
}
|
|
ds.ConditionalAccessMicrosoftGetFunc = func(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error) {
|
|
return nil, ¬FoundError{}
|
|
}
|
|
|
|
_, err := svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{{Name: "team1", Features: tt.spec}}, fleet.ApplyTeamSpecOptions{})
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("Features for existing teams", func(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
spec *json.RawMessage
|
|
old fleet.Features
|
|
result fleet.Features
|
|
}{
|
|
{
|
|
name: "no spec features uses old config",
|
|
spec: nil,
|
|
old: baseFeatures,
|
|
result: baseFeatures,
|
|
},
|
|
{
|
|
name: "missing spec features uses new config default values",
|
|
spec: mkspec(`{"enable_software_inventory": false}`),
|
|
old: baseFeatures,
|
|
result: fleet.Features{
|
|
EnableHostUsers: true,
|
|
EnableSoftwareInventory: false,
|
|
AdditionalQueries: nil,
|
|
},
|
|
},
|
|
{
|
|
name: "config has defaults based on what are the global defaults",
|
|
spec: mkspec(`{"additional_queries": {}}`),
|
|
old: baseFeatures,
|
|
result: fleet.Features{
|
|
EnableHostUsers: true,
|
|
EnableSoftwareInventory: true,
|
|
AdditionalQueries: nil,
|
|
},
|
|
},
|
|
{
|
|
name: "defaults can be overwritten",
|
|
spec: mkspec(`{"enable_host_users": false}`),
|
|
old: baseFeatures,
|
|
result: fleet.Features{
|
|
EnableHostUsers: false,
|
|
EnableSoftwareInventory: false,
|
|
AdditionalQueries: nil,
|
|
},
|
|
},
|
|
{
|
|
name: "all config can be changed",
|
|
spec: mkspec(`{
|
|
"enable_host_users": false,
|
|
"enable_software_inventory": true,
|
|
"additional_queries": {"example": "query"}
|
|
}`),
|
|
old: baseFeatures,
|
|
result: fleet.Features{
|
|
EnableHostUsers: false,
|
|
EnableSoftwareInventory: true,
|
|
AdditionalQueries: ptr.RawMessage([]byte(`{"example": "query"}`)),
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range cases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
|
|
return &fleet.Team{ID: 123, Name: name, Config: fleet.TeamConfig{Features: tt.old}}, nil
|
|
}
|
|
|
|
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
|
return &fleet.Team{ID: 123}, nil
|
|
}
|
|
|
|
opts.ActivityMock.NewActivityFunc = func(_ context.Context, _ *activity_api.User, activity activity_api.ActivityDetails) error {
|
|
act := activity.(fleet.ActivityTypeAppliedSpecTeam)
|
|
require.Len(t, act.Teams, 1)
|
|
return nil
|
|
}
|
|
|
|
idsByTeam, err := svc.ApplyTeamSpecs(
|
|
ctx, []*fleet.TeamSpec{{Name: "team1", Features: tt.spec}}, fleet.ApplyTeamSpecOptions{},
|
|
)
|
|
require.NoError(t, err)
|
|
require.Len(t, idsByTeam, 1)
|
|
require.Equal(t, uint(123), idsByTeam["team1"])
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
// Tests that a new enroll secret is created for new teams when none are provided
|
|
func TestApplyTeamSpecEnrollSecretForNewTeams(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
|
|
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true})
|
|
user := &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}
|
|
ctx = viewer.NewContext(ctx, viewer.Viewer{User: user})
|
|
|
|
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
|
|
return nil, newNotFoundError()
|
|
}
|
|
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{}, nil
|
|
}
|
|
|
|
t.Run("creates enroll secret when not included for a new team spec", func(t *testing.T) {
|
|
ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
|
require.Len(t, team.Secrets, 1)
|
|
require.NotEmpty(t, team.Secrets[0])
|
|
return &fleet.Team{ID: 1}, nil
|
|
}
|
|
|
|
_, err := svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{{Name: "Foo"}}, fleet.ApplyTeamSpecOptions{})
|
|
require.NoError(t, err)
|
|
require.True(t, ds.TeamByNameFuncInvoked)
|
|
require.True(t, ds.NewTeamFuncInvoked)
|
|
})
|
|
|
|
t.Run("does not create enroll secret when one is included for a new team spec", func(t *testing.T) {
|
|
ds.NewTeamFuncInvoked = false
|
|
enrollSecret := fleet.EnrollSecret{Secret: "test"}
|
|
|
|
ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
|
require.Len(t, team.Secrets, 1)
|
|
require.Equal(t, enrollSecret.Secret, team.Secrets[0].Secret)
|
|
return &fleet.Team{ID: 1}, nil
|
|
}
|
|
ds.NewTeamFuncInvoked = false
|
|
|
|
// Dry run -- secret already used
|
|
ds.IsEnrollSecretAvailableFunc = func(ctx context.Context, secret string, isNew bool, teamID *uint) (bool, error) {
|
|
return false, nil
|
|
}
|
|
_, err := svc.ApplyTeamSpecs(
|
|
ctx, []*fleet.TeamSpec{{Name: "Foo", Secrets: &[]fleet.EnrollSecret{enrollSecret}}},
|
|
fleet.ApplyTeamSpecOptions{ApplySpecOptions: fleet.ApplySpecOptions{DryRun: true}},
|
|
)
|
|
assert.ErrorContains(t, err, "is already being used")
|
|
|
|
// Normal dry run
|
|
ds.IsEnrollSecretAvailableFunc = func(ctx context.Context, secret string, isNew bool, teamID *uint) (bool, error) {
|
|
return true, nil
|
|
}
|
|
_, err = svc.ApplyTeamSpecs(
|
|
ctx, []*fleet.TeamSpec{{Name: "Foo", Secrets: &[]fleet.EnrollSecret{enrollSecret}}},
|
|
fleet.ApplyTeamSpecOptions{ApplySpecOptions: fleet.ApplySpecOptions{DryRun: true}},
|
|
)
|
|
assert.NoError(t, err)
|
|
assert.False(t, ds.NewTeamFuncInvoked)
|
|
|
|
_, err = svc.ApplyTeamSpecs(
|
|
ctx, []*fleet.TeamSpec{{Name: "Foo", Secrets: &[]fleet.EnrollSecret{enrollSecret}}}, fleet.ApplyTeamSpecOptions{},
|
|
)
|
|
require.NoError(t, err)
|
|
require.True(t, ds.TeamByNameFuncInvoked)
|
|
require.True(t, ds.NewTeamFuncInvoked)
|
|
})
|
|
|
|
ds.TeamByNameFuncInvoked = false
|
|
ds.NewTeamFuncInvoked = false
|
|
}
|
|
|
|
// TestApplyTeamSpecsErrorInTeamByName tests that an error in ds.TeamByName will
|
|
// result in a proper error returned (instead of the authorization check missing error).
|
|
func TestApplyTeamSpecsErrorInTeamByName(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
|
|
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true})
|
|
user := &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}
|
|
ctx = viewer.NewContext(ctx, viewer.Viewer{User: user})
|
|
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
|
|
return nil, errors.New("unknown error")
|
|
}
|
|
authzctx := &authz_ctx.AuthorizationContext{}
|
|
ctx = authz_ctx.NewContext(ctx, authzctx)
|
|
_, err := svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{{Name: "Foo"}}, fleet.ApplyTeamSpecOptions{})
|
|
require.Error(t, err)
|
|
az, ok := authz_ctx.FromContext(ctx)
|
|
require.True(t, ok)
|
|
require.True(t, az.Checked())
|
|
}
|