New gitops role (#10850)

#8593

This PR adds a new role `gitops` to Fleet.
MDM capabilities for the role coming on a separate PR. We need this
merged ASAP so that we can unblock the UI work for this.

- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [x] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)
- [X] Documented any permissions changes
- ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)~
- ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.~
- [X] Added/updated tests
- [x] Manual QA for all new/changed functionality
  - ~For Orbit and Fleet Desktop changes:~
- ~[ ] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.~
- ~[ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
This commit is contained in:
Lucas Manuel Rodriguez 2023-04-12 16:11:04 -03:00 committed by GitHub
parent 3c177aa928
commit 1ebfbb14eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 2323 additions and 463 deletions

1
changes/8593-gitops Normal file
View File

@ -0,0 +1 @@
* Add `gitops` user role to Fleet. GitOps users are users that can manage configuration.

View File

@ -16,6 +16,7 @@ import (
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/service"
kithttp "github.com/go-kit/kit/transport/http"
"github.com/kolide/kit/version"
"github.com/urfave/cli/v2"
)
@ -81,14 +82,18 @@ func clientFromCLI(c *cli.Context) (*service.Client, error) {
}
// check that AppConfig's Apple BM terms are not expired.
appCfg, err := fleetClient.GetAppConfig()
if err != nil {
var sce kithttp.StatusCoder
switch appCfg, err := fleetClient.GetAppConfig(); {
case err == nil:
if appCfg.MDM.AppleBMTermsExpired {
fleet.WriteAppleBMTermsExpiredBanner(os.Stderr)
// This is just a warning, continue ...
}
case errors.As(err, &sce) && sce.StatusCode() == http.StatusForbidden:
// OK, could be a user without permissions to read app config (e.g. gitops).
default:
return nil, err
}
if appCfg.MDM.AppleBMTermsExpired {
fleet.WriteAppleBMTermsExpiredBanner(os.Stderr)
// This is just a warning, continue ...
}
return fleetClient, nil
}

View File

@ -5,6 +5,7 @@ import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"os"
"testing"
"time"
@ -537,25 +538,8 @@ spec:
assert.True(t, savedAppConfig.Features.EnableSoftwareInventory)
}
func TestApplyPolicies(t *testing.T) {
_, ds := runServerWithMockedDS(t)
var appliedPolicySpecs []*fleet.PolicySpec
ds.ApplyPolicySpecsFunc = func(ctx context.Context, authorID uint, specs []*fleet.PolicySpec) error {
appliedPolicySpecs = specs
return nil
}
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
if name == "Team1" {
return &fleet.Team{ID: 123}, nil
}
return nil, errors.New("unexpected team name!")
}
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
return nil
}
name := writeTmpYml(t, `---
const (
policySpec = `---
apiVersion: v1
kind: policy
spec:
@ -583,7 +567,68 @@ spec:
description: Checks to make sure that the Filevault feature is enabled on macOS devices.
resolution: "Choose Apple menu > System Preferences, then click Security & Privacy. Click the FileVault tab. Click the Lock icon, then enter an administrator name and password. Click Turn On FileVault."
platform: darwin
`)
`
enrollSecretsSpec = `---
apiVersion: v1
kind: enroll_secret
spec:
secrets:
- secret: RzTlxPvugG4o4O5IKS/HqEDJUmI1hwBoffff
- secret: reallyworks
- secret: thissecretwontwork!
`
labelsSpec = `---
apiVersion: v1
kind: label
spec:
name: pending_updates
query: select 1;
platforms:
- darwin
`
packsSpec = `---
apiVersion: v1
kind: pack
spec:
name: osquery_monitoring
queries:
- query: osquery_version
name: osquery_version_snapshot
interval: 7200
snapshot: true
- query: osquery_version
name: osquery_version_differential
interval: 7200
`
queriesSpec = `---
apiVersion: v1
kind: query
spec:
description: Retrieves the list of application scheme/protocol-based IPC handlers.
name: app_schemes
query: select * from app_schemes;
`
)
func TestApplyPolicies(t *testing.T) {
_, ds := runServerWithMockedDS(t)
var appliedPolicySpecs []*fleet.PolicySpec
ds.ApplyPolicySpecsFunc = func(ctx context.Context, authorID uint, specs []*fleet.PolicySpec) error {
appliedPolicySpecs = specs
return nil
}
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
if name == "Team1" {
return &fleet.Team{ID: 123}, nil
}
return nil, errors.New("unexpected team name!")
}
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
return nil
}
name := writeTmpYml(t, policySpec)
assert.Equal(t, "[+] applied 3 policies\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.True(t, ds.ApplyPolicySpecsFuncInvoked)
@ -594,6 +639,201 @@ spec:
assert.True(t, ds.TeamByNameFuncInvoked)
}
func TestApplyAsGitOps(t *testing.T) {
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
_, ds := runServerWithMockedDS(t, &service.TestServerOpts{License: license})
gitOps := &fleet.User{
Name: "GitOps",
Password: []byte("p4ssw0rd.123"),
Email: "gitops1@example.com",
GlobalRole: ptr.String(fleet.RoleGitOps),
}
gitOps, err := ds.NewUser(context.Background(), gitOps)
require.NoError(t, err)
ds.SessionByKeyFunc = func(ctx context.Context, key string) (*fleet.Session, error) {
return &fleet.Session{
CreateTimestamp: fleet.CreateTimestamp{CreatedAt: time.Now()},
ID: 1,
AccessedAt: time.Now(),
UserID: gitOps.ID,
Key: key,
}, nil
}
ds.UserByIDFunc = func(ctx context.Context, id uint) (*fleet.User, error) {
return gitOps, nil
}
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
return nil
}
// Apply global config.
currentAppConfig := &fleet.AppConfig{
OrgInfo: fleet.OrgInfo{OrgName: "Fleet"}, ServerSettings: fleet.ServerSettings{ServerURL: "https://example.org"},
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return currentAppConfig, nil
}
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
currentAppConfig = config
return nil
}
name := writeTmpYml(t, `---
apiVersion: v1
kind: config
spec:
features:
enable_host_users: true
enable_software_inventory: true
agent_options:
config:
decorators:
load:
- SELECT uuid AS host_uuid FROM system_info;
- SELECT hostname AS hostname FROM system_info;
options:
disable_distributed: false
distributed_interval: 10
distributed_plugin: tls
distributed_tls_max_attempts: 3
logger_tls_endpoint: /api/osquery/log
logger_tls_period: 10
pack_delimiter: /
overrides: {}
`)
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.True(t, currentAppConfig.Features.EnableHostUsers)
// Apply team config.
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
if name == "Team1" {
return &fleet.Team{ID: 123}, nil
}
return nil, errors.New("unexpected team name!")
}
var savedTeam *fleet.Team
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
savedTeam = team
return team, nil
}
var teamEnrollSecrets []*fleet.EnrollSecret
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
if teamID == nil || *teamID != 123 {
return fmt.Errorf("unexpected data: %+v", teamID)
}
teamEnrollSecrets = secrets
return nil
}
/*
# TODO(lucas): MDM still not defined.
# mdm:
# macos_updates:
# minimum_version: 10.10.10
# deadline: 1992-03-01
*/
name = writeTmpYml(t, `
apiVersion: v1
kind: team
spec:
team:
agent_options:
config:
views:
foo: qux
name: Team1
secrets:
- secret: BBB
`)
require.Equal(t, "[+] applied 1 teams\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.JSONEq(t, string(json.RawMessage(`{"config":{"views":{"foo":"qux"}}}`)), string(*savedTeam.Config.AgentOptions))
/*
assert.Equal(t, fleet.TeamMDM{
MacOSUpdates: fleet.MacOSUpdates{
MinimumVersion: "10.10.10",
Deadline: "1992-03-01",
},
}, savedTeam.Config.MDM)
*/
assert.Equal(t, []*fleet.EnrollSecret{{Secret: "BBB"}}, teamEnrollSecrets)
assert.True(t, ds.ApplyEnrollSecretsFuncInvoked)
// Apply policies.
var appliedPolicySpecs []*fleet.PolicySpec
ds.ApplyPolicySpecsFunc = func(ctx context.Context, authorID uint, specs []*fleet.PolicySpec) error {
appliedPolicySpecs = specs
return nil
}
name = writeTmpYml(t, policySpec)
assert.Equal(t, "[+] applied 3 policies\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.True(t, ds.ApplyPolicySpecsFuncInvoked)
assert.Len(t, appliedPolicySpecs, 3)
for _, p := range appliedPolicySpecs {
assert.NotEmpty(t, p.Platform)
}
assert.True(t, ds.TeamByNameFuncInvoked)
// Apply enroll secrets.
var appliedSecrets []*fleet.EnrollSecret
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
appliedSecrets = secrets
return nil
}
name = writeTmpYml(t, enrollSecretsSpec)
assert.Equal(t, "[+] applied enroll secrets\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.True(t, ds.ApplyEnrollSecretsFuncInvoked)
assert.Len(t, appliedSecrets, 3)
for _, s := range appliedSecrets {
assert.NotEmpty(t, s.Secret)
}
// Apply labels.
var appliedLabels []*fleet.LabelSpec
ds.ApplyLabelSpecsFunc = func(ctx context.Context, specs []*fleet.LabelSpec) error {
appliedLabels = specs
return nil
}
name = writeTmpYml(t, labelsSpec)
assert.Equal(t, "[+] applied 1 labels\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.True(t, ds.ApplyLabelSpecsFuncInvoked)
require.Len(t, appliedLabels, 1)
assert.Equal(t, "pending_updates", appliedLabels[0].Name)
assert.Equal(t, "select 1;", appliedLabels[0].Query)
// Apply packs.
var appliedPacks []*fleet.PackSpec
ds.ApplyPackSpecsFunc = func(ctx context.Context, specs []*fleet.PackSpec) error {
appliedPacks = specs
return nil
}
ds.ListPacksFunc = func(ctx context.Context, opt fleet.PackListOptions) ([]*fleet.Pack, error) {
return nil, nil
}
name = writeTmpYml(t, packsSpec)
assert.Equal(t, "[+] applied 1 packs\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.True(t, ds.ApplyPackSpecsFuncInvoked)
require.Len(t, appliedPacks, 1)
assert.Equal(t, "osquery_monitoring", appliedPacks[0].Name)
require.Len(t, appliedPacks[0].Queries, 2)
// Apply queries.
var appliedQueries []*fleet.Query
ds.QueryByNameFunc = func(ctx context.Context, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) {
return nil, sql.ErrNoRows
}
ds.ApplyQueriesFunc = func(ctx context.Context, authorID uint, queries []*fleet.Query) error {
appliedQueries = queries
return nil
}
name = writeTmpYml(t, queriesSpec)
assert.Equal(t, "[+] applied 1 queries\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.True(t, ds.ApplyQueriesFuncInvoked)
require.Len(t, appliedQueries, 1)
assert.Equal(t, "app_schemes", appliedQueries[0].Name)
assert.Equal(t, "select * from app_schemes;", appliedQueries[0].Query)
}
func TestApplyEnrollSecrets(t *testing.T) {
_, ds := runServerWithMockedDS(t)
@ -603,15 +843,7 @@ func TestApplyEnrollSecrets(t *testing.T) {
return nil
}
name := writeTmpYml(t, `---
apiVersion: v1
kind: enroll_secret
spec:
secrets:
- secret: RzTlxPvugG4o4O5IKS/HqEDJUmI1hwBoffff
- secret: reallyworks
- secret: thissecretwontwork!
`)
name := writeTmpYml(t, enrollSecretsSpec)
assert.Equal(t, "[+] applied enroll secrets\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.True(t, ds.ApplyEnrollSecretsFuncInvoked)
@ -630,15 +862,7 @@ func TestApplyLabels(t *testing.T) {
return nil
}
name := writeTmpYml(t, `---
apiVersion: v1
kind: label
spec:
name: pending_updates
query: select 1;
platforms:
- darwin
`)
name := writeTmpYml(t, labelsSpec)
assert.Equal(t, "[+] applied 1 labels\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.True(t, ds.ApplyLabelSpecsFuncInvoked)
@ -663,20 +887,7 @@ func TestApplyPacks(t *testing.T) {
return nil
}
name := writeTmpYml(t, `---
apiVersion: v1
kind: pack
spec:
name: osquery_monitoring
queries:
- query: osquery_version
name: osquery_version_snapshot
interval: 7200
snapshot: true
- query: osquery_version
name: osquery_version_differential
interval: 7200
`)
name := writeTmpYml(t, packsSpec)
assert.Equal(t, "[+] applied 1 packs\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.True(t, ds.ApplyPackSpecsFuncInvoked)
@ -720,14 +931,7 @@ func TestApplyQueries(t *testing.T) {
return nil
}
name := writeTmpYml(t, `---
apiVersion: v1
kind: query
spec:
description: Retrieves the list of application scheme/protocol-based IPC handlers.
name: app_schemes
query: select * from app_schemes;
`)
name := writeTmpYml(t, queriesSpec)
assert.Equal(t, "[+] applied 1 queries\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.True(t, ds.ApplyQueriesFuncInvoked)

View File

@ -2,53 +2,76 @@
Users have different abilities depending on the access level they have.
Users with the Admin role receive all permissions.
## Roles
### Admin
Users with the admin role receive all permissions.
### Maintainer
Maintainers can manage most entities in Fleet, like queries, policies, labels and schedules.
Unlike admins, maintainers cannot edit higher level settings like application configuration, teams or users.
### Observer
The Observer role is a read-only role. It can access most entities in Fleet, like queries, policies, labels, schedules, application configuration, teams, etc.
They can also run queries configured with the `observer_can_run` flag set to `true`.
### Observer+
Observer+ is an Observer with the added ability to run *any* query.
### GitOps
GitOps is a modern approach to Continuous Deployment (CD) that uses Git as the single source of truth for declarative infrastructure and application configurations.
GitOps is an API-only and write-only role that can be used on CI/CD pipelines.
## User permissions
| **Action** | Observer | Observer+ | Maintainer | Admin |
| ------------------------------------------------------------------------------------------------------------------------------------------ | -------- | --------- | ---------- | ----- |
| View all [activity](https://fleetdm.com/docs/using-fleet/rest-api#activities) | ✅ | ✅ | ✅ | ✅ |
| View all hosts | ✅ | ✅ | ✅ | ✅ |
| Filter hosts using [labels](https://fleetdm.com/docs/using-fleet/rest-api#labels) | ✅ | ✅ | ✅ | ✅ |
| Target hosts using labels | ✅ | ✅ | ✅ | ✅ |
| Add and delete hosts | | | ✅ | ✅ |
| Transfer hosts between teams\* | | | ✅ | ✅ |
| Create, edit, and delete labels | | | ✅ | ✅ |
| View all software | ✅ | ✅ | ✅ | ✅ |
| Filter software by [vulnerabilities](https://fleetdm.com/docs/using-fleet/vulnerability-processing#vulnerability-processing) | ✅ | ✅ | ✅ | ✅ |
| Filter hosts by software | ✅ | ✅ | ✅ | ✅ |
| Filter software by team\* | ✅ | ✅ | ✅ | ✅ |
| Manage [vulnerability automations](https://fleetdm.com/docs/using-fleet/automations#vulnerability-automations) | | | | ✅ |
| Run only designated, **observer can run** ,queries as live queries against all hosts | ✅ | ✅ | ✅ | ✅ |
| Run any query as [live query](https://fleetdm.com/docs/using-fleet/fleet-ui#run-a-query) against all hosts | | ✅ | ✅ | ✅ |
| Create, edit, and delete queries | | | ✅ | ✅ |
| View all queries | ✅ | ✅ | ✅ | ✅ |
| Add, edit, and remove queries from all schedules | | | ✅ | ✅ |
| Create, edit, view, and delete packs | | | ✅ | ✅ |
| View all policies | ✅ | ✅ | ✅ | ✅ |
| Filter hosts using policies | ✅ | ✅ | ✅ | ✅ |
| Create, edit, and delete policies for all hosts | | | ✅ | ✅ |
| Create, edit, and delete policies for all hosts assigned to team\* | | | ✅ | ✅ |
| Manage [policy automations](https://fleetdm.com/docs/using-fleet/automations#policy-automations) | | | | ✅ |
| Create, edit, view, and delete users | | | | ✅ |
| Add and remove team members\* | | | | ✅ |
| Create, edit, and delete teams\* | | | | ✅ |
| Create, edit, and delete [enroll secrets](https://fleetdm.com/docs/deploying/faq#when-do-i-need-to-deploy-a-new-enroll-secret-to-my-hosts) | | | ✅ | ✅ |
| Create, edit, and delete [enroll secrets for teams](https://fleetdm.com/docs/using-fleet/rest-api#get-enroll-secrets-for-a-team)\* | | | ✅ | ✅ |
| Read organization settings and agent options\** | ✅ | ✅ | ✅ | ✅ |
| Edit [organization settings](https://fleetdm.com/docs/using-fleet/configuration-files#organization-settings) | | | | ✅ |
| Edit [agent options](https://fleetdm.com/docs/using-fleet/configuration-files#agent-options) | | | | ✅ |
| Edit [agent options for hosts assigned to teams](https://fleetdm.com/docs/using-fleet/configuration-files#team-agent-options)\* | | | | ✅ |
| Initiate [file carving](https://fleetdm.com/docs/using-fleet/rest-api#file-carving) | | | ✅ | ✅ |
| Retrieve contents from file carving | | | | ✅ |
| View Apple mobile device management (MDM) certificate information | | | | ✅ |
| View Apple business manager (BM) information | | | | ✅ |
| Generate Apple mobile device management (MDM) certificate signing request (CSR) | | | | ✅ |
| View disk encryption key for macOS hosts enrolled in Fleet's MDM | ✅ | ✅ | ✅ | ✅ |
| Create edit and delete configuration profiles for macOS hosts enrolled in Fleet's MDM | | | ✅ | ✅ |
| Execute MDM commands on macOS hosts enrolled in Fleet's MDM | | | ✅ | ✅ |
| View results of MDM commands executed on macOS hosts enrolled in Fleet's MDM | ✅ | ✅ | ✅ | ✅ |
| **Action** | Observer | Observer+ | Maintainer | Admin | GitOps |
| ------------------------------------------------------------------------------------------------------------------------------------------ | -------- | --------- | ---------- | ----- | ------ |
| View all [activity](https://fleetdm.com/docs/using-fleet/rest-api#activities) | ✅ | ✅ | ✅ | ✅ | |
| View all hosts | ✅ | ✅ | ✅ | ✅ | |
| Filter hosts using [labels](https://fleetdm.com/docs/using-fleet/rest-api#labels) | ✅ | ✅ | ✅ | ✅ | |
| Target hosts using labels | ✅ | ✅ | ✅ | ✅ | |
| Add and delete hosts | | | ✅ | ✅ | |
| Transfer hosts between teams\* | | | ✅ | ✅ | ✅ |
| Create, edit, and delete labels | | | ✅ | ✅ | ✅ |
| View all software | ✅ | ✅ | ✅ | ✅ | |
| Filter software by [vulnerabilities](https://fleetdm.com/docs/using-fleet/vulnerability-processing#vulnerability-processing) | ✅ | ✅ | ✅ | ✅ | |
| Filter hosts by software | ✅ | ✅ | ✅ | ✅ | |
| Filter software by team\* | ✅ | ✅ | ✅ | ✅ | |
| Manage [vulnerability automations](https://fleetdm.com/docs/using-fleet/automations#vulnerability-automations) | | | | ✅ | ✅ |
| Run only designated, **observer can run** ,queries as live queries against all hosts | ✅ | ✅ | ✅ | ✅ | |
| Run any query as [live query](https://fleetdm.com/docs/using-fleet/fleet-ui#run-a-query) against all hosts | | ✅ | ✅ | ✅ | |
| Create, edit, and delete queries | | | ✅ | ✅ | ✅ |
| View all queries | ✅ | ✅ | ✅ | ✅ | |
| Add, edit, and remove queries from all schedules | | | ✅ | ✅ | ✅ |
| Create, edit, view, and delete packs | | | ✅ | ✅ | ✅ |
| View all policies | ✅ | ✅ | ✅ | ✅ | |
| Filter hosts using policies | ✅ | ✅ | ✅ | ✅ | |
| Create, edit, and delete policies for all hosts | | | ✅ | ✅ | ✅ |
| Create, edit, and delete policies for all hosts assigned to team\* | | | ✅ | ✅ | ✅ |
| Manage [policy automations](https://fleetdm.com/docs/using-fleet/automations#policy-automations) | | | | ✅ | ✅ |
| Create, edit, view, and delete users | | | | ✅ | |
| Add and remove team members\* | | | | ✅ | ✅ |
| Create, edit, and delete teams\* | | | | ✅ | ✅ |
| Create, edit, and delete [enroll secrets](https://fleetdm.com/docs/deploying/faq#when-do-i-need-to-deploy-a-new-enroll-secret-to-my-hosts) | | | ✅ | ✅ | ✅ |
| Create, edit, and delete [enroll secrets for teams](https://fleetdm.com/docs/using-fleet/rest-api#get-enroll-secrets-for-a-team)\* | | | ✅ | ✅ | |
| Read organization settings and agent options\** | ✅ | ✅ | ✅ | ✅ | |
| Edit [organization settings](https://fleetdm.com/docs/using-fleet/configuration-files#organization-settings) | | | | ✅ | ✅ |
| Edit [agent options](https://fleetdm.com/docs/using-fleet/configuration-files#agent-options) | | | | ✅ | ✅ |
| Edit [agent options for hosts assigned to teams](https://fleetdm.com/docs/using-fleet/configuration-files#team-agent-options)\* | | | | ✅ | ✅ |
| Initiate [file carving](https://fleetdm.com/docs/using-fleet/rest-api#file-carving) | | | ✅ | ✅ | |
| Retrieve contents from file carving | | | | ✅ | |
| View Apple mobile device management (MDM) certificate information | | | | ✅ | |
| View Apple business manager (BM) information | | | | ✅ | |
| Generate Apple mobile device management (MDM) certificate signing request (CSR) | | | | ✅ | |
| View disk encryption key for macOS hosts enrolled in Fleet's MDM | ✅ | ✅ | ✅ | ✅ | |
| Create edit and delete configuration profiles for macOS hosts enrolled in Fleet's MDM | | | ✅ | ✅ | |
| Execute MDM commands on macOS hosts enrolled in Fleet's MDM | | | ✅ | ✅ | |
| View results of MDM commands executed on macOS hosts enrolled in Fleet's MDM | ✅ | ✅ | ✅ | ✅ | |
\*Applies only to Fleet Premium
@ -71,34 +94,35 @@ Users can be a member of multiple teams in Fleet.
Users that are members of multiple teams can be assigned different roles for each team. For example, a user can be given access to the "Workstations" team and assigned the "Observer" role. This same user can be given access to the "Servers" team and assigned the "Maintainer" role.
| **Action** | Team observer | Team observer+ | Team maintainer | Team admin |
| -------------------------------------------------------------------------------------------------------------------------------- | ------------- | -------------- | --------------- | ---------- |
| View hosts | ✅ | ✅ | ✅ | ✅ |
| Filter hosts using [labels](https://fleetdm.com/docs/using-fleet/rest-api#labels) | ✅ | ✅ | ✅ | ✅ |
| Target hosts using labels | ✅ | ✅ | ✅ | ✅ |
| Add and delete hosts | | | ✅ | ✅ |
| Filter software by [vulnerabilities](<(https://fleetdm.com/docs/using-fleet/vulnerability-processing#vulnerability-processing)>) | ✅ | ✅ | ✅ | ✅ |
| Filter hosts by software | ✅ | ✅ | ✅ | ✅ |
| Filter software | ✅ | ✅ | ✅ | ✅ |
| Run only designated, **observer can run** ,queries as live queries against all hosts | ✅ | ✅ | ✅ | ✅ |
| Run any query as [live query](https://fleetdm.com/docs/using-fleet/fleet-ui#run-a-query) | | ✅ | ✅ | ✅ |
| Create, edit, and delete only **self authored** queries | | | ✅ | ✅ |
| Add, edit, and remove queries from the schedule | | | ✅ | ✅ |
| View policies | ✅ | ✅ | ✅ | ✅ |
| View global (inherited) policies | ✅ | ✅ | ✅ | ✅ |
| Filter hosts using policies | ✅ | ✅ | ✅ | ✅ |
| Create, edit, and delete policies | | | ✅ | ✅ |
| Manage [policy automations](https://fleetdm.com/docs/using-fleet/automations#policy-automations) | | | | ✅ |
| Add and remove team members | | | | ✅ |
| Edit team name | | | | ✅ |
| Create, edit, and delete [team enroll secrets](https://fleetdm.com/docs/using-fleet/rest-api#get-enroll-secrets-for-a-team) | | | ✅ | ✅ |
| Read agent options\* | ✅ | ✅ | ✅ | ✅ |
| Edit [agent options](https://fleetdm.com/docs/using-fleet/configuration-files#agent-options) | | | | ✅ |
| Initiate [file carving](https://fleetdm.com/docs/using-fleet/rest-api#file-carving) | | | ✅ | ✅ |
| View disk encryption key for macOS hosts enrolled in Fleet's MDM | ✅ | ✅ | ✅ | ✅ |
| Create edit and delete configuration profiles for macOS hosts enrolled in Fleet's MDM | | | ✅ | ✅ |
| Execute MDM commands on macOS hosts enrolled in Fleet's MDM | | | ✅ | ✅ |
| View results of MDM commands executed on macOS hosts enrolled in Fleet's MDM | ✅ | ✅ | ✅ | ✅ |
| **Action** | Team observer | Team observer+ | Team maintainer | Team admin | Team GitOps |
| -------------------------------------------------------------------------------------------------------------------------------- | ------------- | -------------- | --------------- | ---------- | ----------- |
| View hosts | ✅ | ✅ | ✅ | ✅ | |
| Filter hosts using [labels](https://fleetdm.com/docs/using-fleet/rest-api#labels) | ✅ | ✅ | ✅ | ✅ | |
| Target hosts using labels | ✅ | ✅ | ✅ | ✅ | |
| Add and delete hosts | | | ✅ | ✅ | |
| Filter software by [vulnerabilities](<(https://fleetdm.com/docs/using-fleet/vulnerability-processing#vulnerability-processing)>) | ✅ | ✅ | ✅ | ✅ | |
| Filter hosts by software | ✅ | ✅ | ✅ | ✅ | |
| Filter software | ✅ | ✅ | ✅ | ✅ | |
| Run only designated, **observer can run** ,queries as live queries against all hosts | ✅ | ✅ | ✅ | ✅ | |
| Run any query as [live query](https://fleetdm.com/docs/using-fleet/fleet-ui#run-a-query) | | ✅ | ✅ | ✅ | |
| Create, edit, and delete only **self authored** queries | | | ✅ | ✅ | ✅ |
| Add, edit, and remove queries from the schedule | | | ✅ | ✅ | ✅ |
| View policies | ✅ | ✅ | ✅ | ✅ | |
| View global (inherited) policies | ✅ | ✅ | ✅ | ✅ | |
| Filter hosts using policies | ✅ | ✅ | ✅ | ✅ | |
| Create, edit, and delete policies | | | ✅ | ✅ | ✅ |
| Manage [policy automations](https://fleetdm.com/docs/using-fleet/automations#policy-automations) | | | | ✅ | ✅ |
| Add and remove team members | | | | ✅ | ✅ |
| Edit team name | | | | ✅ | ✅ |
| Create, edit, and delete [team enroll secrets](https://fleetdm.com/docs/using-fleet/rest-api#get-enroll-secrets-for-a-team) | | | ✅ | ✅ | |
| Read agent options\* | ✅ | ✅ | ✅ | ✅ | |
| Edit [agent options](https://fleetdm.com/docs/using-fleet/configuration-files#agent-options) | | | | ✅ | ✅ |
| Initiate [file carving](https://fleetdm.com/docs/using-fleet/rest-api#file-carving) | | | ✅ | ✅ | |
| View disk encryption key for macOS hosts enrolled in Fleet's MDM | ✅ | ✅ | ✅ | ✅ | |
| Create edit and delete configuration profiles for macOS hosts enrolled in Fleet's MDM | | | ✅ | ✅ | |
| Execute MDM commands on macOS hosts enrolled in Fleet's MDM, and read command results | | | ✅ | ✅ | |
| Execute MDM commands on macOS hosts enrolled in Fleet's MDM | | | ✅ | ✅ | |
| View results of MDM commands executed on macOS hosts enrolled in Fleet's MDM | ✅ | ✅ | ✅ | ✅ | |
\* Applies only to [Fleet REST API](https://fleetdm.com/docs/using-fleet/rest-api)

View File

@ -6431,8 +6431,8 @@ Creates a user account after an invited user provides registration information a
| name | string | body | **Required**. The name of the user. |
| password | string | body | The password chosen by the user (if not SSO user). |
| password_confirmation | string | body | Confirmation of the password chosen by the user. |
| global_role | string | body | The role assigned to the user. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). In Fleet 4.30.0, the `observer_plus` role was introduced. If `global_role` is specified, `teams` cannot be specified. |
| teams | array | body | _Available in Fleet Premium_ The teams and respective roles assigned to the user. Should contain an array of objects in which each object includes the team's `id` and the user's `role` on each team. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). In Fleet 4.30.0, the `observer_plus` role was introduced. If `teams` is specified, `global_role` cannot be specified. |
| global_role | string | body | The role assigned to the user. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). In Fleet 4.30.0 and 4.31.0, the `observer_plus` and `gitops` roles were introduced respectively. If `global_role` is specified, `teams` cannot be specified. For more information, see [Permissions](Permissions.md). |
| teams | array | body | _Available in Fleet Premium_ The teams and respective roles assigned to the user. Should contain an array of objects in which each object includes the team's `id` and the user's `role` on each team. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). In Fleet 4.30.0 and 4.31.0, the `observer_plus` and `gitops` roles were introduced respectively. If `teams` is specified, `global_role` cannot be specified. For more information, see [Permissions](Permissions.md). |
#### Example
@ -6548,9 +6548,9 @@ By default, the user will be forced to reset its password upon first login.
| password | string | body | The user's password (required for non-SSO users). |
| sso_enabled | boolean | body | Whether or not SSO is enabled for the user. |
| api_only | boolean | body | User is an "API-only" user (cannot use web UI) if true. |
| global_role | string | body | The role assigned to the user. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). In Fleet 4.30.0, the `observer_plus` role was introduced. If `global_role` is specified, `teams` cannot be specified. |
| global_role | string | body | The role assigned to the user. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). In Fleet 4.30.0 and 4.31.0, the `observer_plus` and `gitops` roles were introduced respectively. If `global_role` is specified, `teams` cannot be specified. For more information, see [Permissions](Permissions.md). |
| admin_forced_password_reset | boolean | body | Sets whether the user will be forced to reset its password upon first login (default=true) |
| teams | array | body | _Available in Fleet Premium_ The teams and respective roles assigned to the user. Should contain an array of objects in which each object includes the team's `id` and the user's `role` on each team. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). In Fleet 4.30.0, the `observer_plus` role was introduced. If `teams` is specified, `global_role` cannot be specified. |
| teams | array | body | _Available in Fleet Premium_ The teams and respective roles assigned to the user. Should contain an array of objects in which each object includes the team's `id` and the user's `role` on each team. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). In Fleet 4.30.0 and 4.31.0, the `observer_plus` and `gitops` roles were introduced respectively. If `teams` is specified, `global_role` cannot be specified. For more information, see [Permissions](Permissions.md). |
#### Example

View File

@ -11,6 +11,7 @@ import (
"github.com/fleetdm/fleet/v4/server"
"github.com/fleetdm/fleet/v4/server/authz"
authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/contexts/logging"
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
@ -499,23 +500,30 @@ func (svc *Service) teamByIDOrName(ctx context.Context, id *uint, name *string)
var jsonNull = json.RawMessage(`null`)
func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec, applyOpts fleet.ApplySpecOptions) error {
if err := svc.authz.Authorize(ctx, &fleet.Team{}, fleet.ActionRead); err != nil {
return err
// setAuthCheckedOnPreAuthErr can be used to set the authentication as checked
// in case of errors that happened before an auth check can be performed.
// Otherwise the endpoints return a "authentication skipped" error instead of
// the actual returned error.
func setAuthCheckedOnPreAuthErr(ctx context.Context) {
if az, ok := authz_ctx.FromContext(ctx); ok {
az.SetChecked()
}
}
// check auth for all teams specified first
func (svc *Service) checkAuthorizationForTeams(ctx context.Context, specs []*fleet.TeamSpec) error {
for _, spec := range specs {
team, err := svc.ds.TeamByName(ctx, spec.Name)
if err != nil {
if err := ctxerr.Cause(err); err == sql.ErrNoRows {
// can the user create a new team?
// Can the user create a new team?
if err := svc.authz.Authorize(ctx, &fleet.Team{}, fleet.ActionWrite); err != nil {
return err
}
continue
}
// Set authorization as checked to return a proper error.
setAuthCheckedOnPreAuthErr(ctx)
return err
}
@ -524,11 +532,25 @@ func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec,
return err
}
}
return nil
}
appConfig, err := svc.AppConfigObfuscated(ctx)
func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec, applyOpts fleet.ApplySpecOptions) error {
if len(specs) == 0 {
setAuthCheckedOnPreAuthErr(ctx)
// Nothing to do.
return nil
}
if err := svc.checkAuthorizationForTeams(ctx, specs); err != nil {
return err
}
appConfig, err := svc.ds.AppConfig(ctx)
if err != nil {
return err
}
appConfig.Obfuscate()
var details []fleet.TeamActivityDetail

View File

@ -31,6 +31,7 @@ admin := "admin"
maintainer := "maintainer"
observer := "observer"
observer_plus := "observer_plus"
gitops := "gitops"
# Default deny
default allow = false
@ -47,17 +48,25 @@ team_role(subject, team_id) = role {
# Global config
##
# Any logged in user can read global config
# Global admin, maintainer, observer_plus and observer can read global config.
allow {
object.type == "app_config"
not is_null(subject)
subject.global_role == [admin, maintainer, observer_plus, observer][_]
action == read
}
# Admin can write global config
# Team admin, maintainer, observer_plus and observer can read global config.
allow {
object.type == "app_config"
subject.global_role == admin
# If role is admin, maintainer, observer_plus or observer on any team.
team_role(subject, subject.teams[_].id) == [admin, maintainer, observer_plus, observer][_]
action == read
}
# Global admins and gitops can write global config.
allow {
object.type == "app_config"
subject.global_role == [admin, gitops][_]
action == write
}
@ -73,14 +82,8 @@ allow {
not is_null(subject)
action == read
}
# For specific teams, only members can read.
allow {
object.type == "team"
object.id != 0
team_role(subject, object.id) == [admin, maintainer, observer, observer_plus][_]
action == read
}
# Global users can read all teams.
# Global admins, maintainers, observer_plus and observers can read teams.
allow {
object.type == "team"
object.id != 0
@ -88,17 +91,25 @@ allow {
action == read
}
# Admin can write teams
# Team admins, maintainers, observer_plus and observers can read their team.
allow {
object.type == "team"
subject.global_role == admin
object.id != 0
team_role(subject, object.id) == [admin, maintainer, observer, observer_plus][_]
action == read
}
# Global admins and gitops can write teams.
allow {
object.type == "team"
subject.global_role == [admin, gitops][_]
action == write
}
# Team admin can write teams
# Team admins and gitops can write their teams.
allow {
object.type == "team"
team_role(subject, object.id) == admin
team_role(subject, object.id) == [admin, gitops][_]
action == write
}
@ -146,10 +157,10 @@ allow {
# Activities
##
# Only global users can read activities
# Global admins, maintainers, observer_plus and observers can read activities.
allow {
not is_null(subject.global_role)
object.type == "activity"
subject.global_role == [admin, maintainer, observer_plus, observer][_]
action == read
}
@ -175,14 +186,22 @@ allow {
# Enroll Secrets
##
# Global admins and maintainers can read/write all
# Global admins and maintainers can read/write enroll secrets.
allow {
object.type == "enroll_secret"
subject.global_role == [admin, maintainer][_]
action == [read, write][_]
}
# Team admins and maintainers can read/write for appropriate teams
# Global gitops can write global enroll secrets.
allow {
object.type == "enroll_secret"
object.is_global_secret
subject.global_role == gitops
action == write
}
# Team admins and maintainers can read/write for appropriate teams.
allow {
object.type == "enroll_secret"
team_role(subject, object.team_id) == [admin, maintainer][_]
@ -195,23 +214,33 @@ allow {
# Hosts
##
# Allow anyone to list (must be filtered appropriately by the service).
# Global admins, maintainers, observer_plus and observers can list hosts.
allow {
object.type == "host"
not is_null(subject)
subject.global_role == [admin, maintainer, observer_plus, observer][_]
action == list
}
# Allow read/write for global admin/maintainer
# Team admins, maintainers, observer_plus and observers can list hosts.
allow {
object.type == "host"
subject.global_role = admin
# If role is admin, maintainer, observer_plus or observer on any team.
team_role(subject, subject.teams[_].id) == [admin, maintainer, observer_plus, observer][_]
action == list
}
# Allow read/write for global admin/maintainer.
allow {
object.type == "host"
subject.global_role == [admin, maintainer][_]
action == [read, write][_]
}
# Global gitops can write hosts.
allow {
object.type == "host"
subject.global_role = maintainer
action == [read, write][_]
subject.global_role == gitops
action == write
}
# Allow read for global observer and observer_plus.
@ -239,22 +268,25 @@ allow {
# Labels
##
# All users can read labels
# Global admins, maintainers, observer_plus and observers can read labels.
allow {
object.type == "label"
not is_null(subject)
subject.global_role == [admin, maintainer, observer_plus, observer][_]
action == read
}
# Only global admins and maintainers can write labels
# Team admins, maintainers, observer_plus and observers can read labels.
allow {
object.type == "label"
subject.global_role == admin
action == write
object.type == "label"
# If role is admin, maintainer, observer_plus or observer on any team.
team_role(subject, subject.teams[_].id) == [admin, maintainer, observer_plus, observer][_]
action == read
}
# Only global admins, maintainers and gitops can write labels
allow {
object.type == "label"
subject.global_role == maintainer
subject.global_role == [admin, maintainer, gitops][_]
action == write
}
@ -262,33 +294,41 @@ allow {
# Queries
##
# All users can read queries
# Global admins, maintainers, observer_plus and observers can read queries.
allow {
not is_null(subject)
object.type == "query"
subject.global_role == [admin, maintainer, observer_plus, observer][_]
action == read
}
# Global admins and maintainers can write queries
# Team admins, maintainers, observer_plus and observers can read queries.
allow {
object.type == "query"
# If role is admin, maintainer, observer_plus or observer on any team.
team_role(subject, subject.teams[_].id) == [admin, maintainer, observer_plus, observer][_]
action == read
}
# Global admins, maintainers and gitops can write queries.
allow {
object.type == "query"
subject.global_role == [admin, maintainer][_]
subject.global_role == [admin, maintainer, gitops][_]
action == write
}
# Team admins and maintainers can create new queries
# Team admins, maintainers and gitops can create new queries
allow {
object.id == 0 # new queries have ID zero
object.type == "query"
team_role(subject, subject.teams[_].id) == [admin, maintainer][_]
team_role(subject, subject.teams[_].id) == [admin, maintainer, gitops][_]
action == write
}
# Team admins and maintainers can edit and delete only their own queries
# Team admins, maintainers and gitops can edit and delete only their own queries
allow {
object.author_id == subject.id
object.type == "query"
team_role(subject, subject.teams[_].id) == [admin,maintainer][_]
team_role(subject, subject.teams[_].id) == [admin, maintainer, gitops][_]
action == write
}
@ -390,22 +430,31 @@ allow {
# Packs
##
# Global admins and maintainers can read/write all packs.
# Global admins, maintainers and gitops can read/write all types of packs.
allow {
object.type == "pack"
subject.global_role == [admin, maintainer][_]
subject.global_role == [admin, maintainer, gitops][_]
action == [read, write][_]
}
# All users can read the global pack.
# Global admins, maintainers, observers and observer_plus can read the global pack.
allow {
object.type == "pack"
not is_null(subject)
object.is_global_pack == true
subject.global_role == [admin, maintainer, observer, observer_plus][_]
action == read
}
# Team admins, maintainers, observers and observer_plus can read their team's pack.
# Team admins, maintainers, observer_plus and observers can read the global pack.
allow {
object.type == "pack"
object.is_global_pack == true
# If role is admin, maintainer, observer_plus or observer on any team.
team_role(subject, subject.teams[_].id) == [admin, maintainer, observer_plus, observer][_]
action == read
}
# Team admins, maintainers, observers, observer_plus can read their team's pack.
#
# NOTE: Action "read" on a team's pack includes listing its scheduled queries.
allow {
@ -415,7 +464,7 @@ allow {
action == read
}
# Team admins and maintainers can add/remove scheduled queries from/to their team's pack.
# Team admins, maintainers and gitops can add/remove scheduled queries from/to their team's pack.
#
# NOTE: The team's pack is not editable per-se, it's a special pack to group
# all the team's scheduled queries. So the "write" operation only covers
@ -423,7 +472,7 @@ allow {
allow {
object.type == "pack"
not is_null(object.pack_team_id)
team_role(subject, object.pack_team_id) == [admin, maintainer][_]
team_role(subject, object.pack_team_id) == [admin, maintainer, gitops][_]
action == write
}
@ -442,29 +491,44 @@ allow {
# Policies
##
# Global Admin and Maintainer can read and write policies
# Global admins and maintainers can read and write policies.
allow {
object.type == "policy"
subject.global_role == [admin,maintainer][_]
subject.global_role == [admin, maintainer][_]
action == [read, write][_]
}
# Global observer and observer_plus can read any policies
# Global gitops can write policies.
allow {
object.type == "policy"
subject.global_role == gitops
action == write
}
# Global observer and observer_plus can read any policies.
allow {
object.type == "policy"
subject.global_role == [observer, observer_plus][_]
action == read
}
# Team admin and maintainers can read and write policies for their teams
# Team admin and maintainers can read and write policies for their teams.
allow {
not is_null(object.team_id)
object.type == "policy"
team_role(subject, object.team_id) == [admin,maintainer][_]
team_role(subject, object.team_id) == [admin, maintainer][_]
action == [read, write][_]
}
# Team admin, maintainers, observers and observer_plus can read global policies.
# Team gitops can write policies for their teams.
allow {
not is_null(object.team_id)
object.type == "policy"
team_role(subject, object.team_id) == gitops
action == write
}
# Team admin, maintainers, observers and observers_plus can read global policies
allow {
is_null(object.team_id)
object.type == "policy"
@ -484,14 +548,14 @@ allow {
# Software
##
# Global users can read all software.
# Global admins, maintainers, observers and observer_plus can read all software.
allow {
object.type == "software_inventory"
subject.global_role == [admin, maintainer, observer, observer_plus][_]
action == read
}
# Team users can read all software in their teams.
# Team admins, maintainers, observers and observer_plus can read all software in their teams.
allow {
not is_null(object.team_id)
object.type == "software_inventory"
@ -637,3 +701,13 @@ allow {
action == [read, write][_]
}
##
# Version
##
# Any logged in user can read Fleet's version
allow {
object.type == "version"
not is_null(subject)
action == read
}

View File

@ -49,7 +49,7 @@ func TestAuthorizeAppConfig(t *testing.T) {
{user: nil, object: config, action: read, allow: false},
{user: nil, object: config, action: write, allow: false},
{user: test.UserNoRoles, object: config, action: read, allow: true},
{user: test.UserNoRoles, object: config, action: read, allow: false},
{user: test.UserNoRoles, object: config, action: write, allow: false},
{user: test.UserAdmin, object: config, action: read, allow: true},
@ -63,6 +63,24 @@ func TestAuthorizeAppConfig(t *testing.T) {
{user: test.UserObserverPlus, object: config, action: read, allow: true},
{user: test.UserObserverPlus, object: config, action: write, allow: false},
{user: test.UserGitOps, object: config, action: read, allow: false},
{user: test.UserGitOps, object: config, action: write, allow: true},
{user: test.UserTeamAdminTeam1, object: config, action: read, allow: true},
{user: test.UserTeamAdminTeam1, object: config, action: write, allow: false},
{user: test.UserTeamMaintainerTeam1, object: config, action: read, allow: true},
{user: test.UserTeamMaintainerTeam1, object: config, action: write, allow: false},
{user: test.UserTeamObserverTeam1, object: config, action: read, allow: true},
{user: test.UserTeamObserverTeam1, object: config, action: write, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: config, action: read, allow: true},
{user: test.UserTeamObserverPlusTeam1, object: config, action: write, allow: false},
{user: test.UserTeamGitOpsTeam1, object: config, action: read, allow: false},
{user: test.UserTeamGitOpsTeam1, object: config, action: write, allow: false},
})
}
@ -101,6 +119,29 @@ func TestAuthorizeSession(t *testing.T) {
})
}
func TestAuthorizeActivity(t *testing.T) {
t.Parallel()
activity := &fleet.Activity{}
runTestCases(t, []authTestCase{
// All global roles except GitOps can read activities.
{user: nil, object: activity, action: read, allow: false},
{user: test.UserAdmin, object: activity, action: read, allow: true},
{user: test.UserMaintainer, object: activity, action: read, allow: true},
{user: test.UserObserver, object: activity, action: read, allow: true},
{user: test.UserObserverPlus, object: activity, action: read, allow: true},
{user: test.UserGitOps, object: activity, action: read, allow: false},
// Team roles cannot read activites.
{user: test.UserTeamAdminTeam1, object: activity, action: read, allow: false},
{user: test.UserTeamMaintainerTeam1, object: activity, action: read, allow: false},
{user: test.UserTeamObserverTeam1, object: activity, action: read, allow: false},
{user: test.UserTeamObserverTeam1, object: activity, action: read, allow: false},
{user: test.UserTeamGitOpsTeam1, object: activity, action: read, allow: false},
})
}
func TestAuthorizeUser(t *testing.T) {
t.Parallel()
@ -243,7 +284,6 @@ func TestAuthorizeEnrollSecret(t *testing.T) {
{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin},
},
}
teamMaintainer := &fleet.User{
Teams: []fleet.UserTeam{
{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer},
@ -254,7 +294,17 @@ func TestAuthorizeEnrollSecret(t *testing.T) {
{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver},
},
}
globalSecret := &fleet.EnrollSecret{}
teamObserverPlus := &fleet.User{
Teams: []fleet.UserTeam{
{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserverPlus},
},
}
teamGitOps := &fleet.User{
Teams: []fleet.UserTeam{
{Team: fleet.Team{ID: 1}, Role: fleet.RoleGitOps},
},
}
globalSecret := &fleet.EnrollSecret{TeamID: nil}
teamSecret := &fleet.EnrollSecret{TeamID: ptr.Uint(1)}
runTestCases(t, []authTestCase{
// No access
@ -278,28 +328,45 @@ func TestAuthorizeEnrollSecret(t *testing.T) {
{user: teamObserver, object: globalSecret, action: write, allow: false},
{user: teamObserver, object: teamSecret, action: read, allow: false},
{user: teamObserver, object: teamSecret, action: write, allow: false},
{user: teamObserverPlus, object: globalSecret, action: read, allow: false},
{user: teamObserverPlus, object: globalSecret, action: write, allow: false},
{user: teamObserverPlus, object: teamSecret, action: read, allow: false},
{user: teamObserverPlus, object: teamSecret, action: write, allow: false},
{user: teamGitOps, object: globalSecret, action: read, allow: false},
{user: teamGitOps, object: globalSecret, action: write, allow: false},
{user: teamGitOps, object: teamSecret, action: read, allow: false},
{user: teamGitOps, object: teamSecret, action: write, allow: false},
// Admin can read/write all
// Global admin can read/write all.
{user: test.UserAdmin, object: globalSecret, action: read, allow: true},
{user: test.UserAdmin, object: globalSecret, action: write, allow: true},
{user: test.UserAdmin, object: teamSecret, action: read, allow: true},
{user: test.UserAdmin, object: teamSecret, action: write, allow: true},
// Maintainer can read all
// Global maintainer can read/write all.
{user: test.UserMaintainer, object: globalSecret, action: read, allow: true},
{user: test.UserMaintainer, object: globalSecret, action: write, allow: true},
{user: test.UserMaintainer, object: teamSecret, action: read, allow: true},
{user: test.UserMaintainer, object: teamSecret, action: write, allow: true},
// Team admin can read/write team secret
// Global GitOps can write global secret but not read it.
{user: test.UserGitOps, object: globalSecret, action: read, allow: false},
{user: test.UserGitOps, object: globalSecret, action: write, allow: true},
// Global GitOps cannot read/write team secrets.
{user: test.UserGitOps, object: teamSecret, action: read, allow: false},
{user: test.UserGitOps, object: teamSecret, action: write, allow: false},
// Team admin cannot read/write global secret.
{user: teamAdmin, object: globalSecret, action: read, allow: false},
{user: teamAdmin, object: globalSecret, action: write, allow: false},
// Team admin can read/write team secret.
{user: teamAdmin, object: teamSecret, action: read, allow: true},
{user: teamAdmin, object: teamSecret, action: write, allow: true},
// Team maintainer can read/write team secret
// Team maintainer cannot read/write global secret.
{user: teamMaintainer, object: globalSecret, action: read, allow: false},
{user: teamMaintainer, object: globalSecret, action: write, allow: false},
// Team maintainer can read/write team secret.
{user: teamMaintainer, object: teamSecret, action: read, allow: true},
{user: teamMaintainer, object: teamSecret, action: write, allow: true},
})
@ -308,25 +375,78 @@ func TestAuthorizeEnrollSecret(t *testing.T) {
func TestAuthorizeTeam(t *testing.T) {
t.Parallel()
team := &fleet.Team{}
team := &fleet.Team{} // Empty team is used to "list teams"
team1 := &fleet.Team{ID: 1}
team2 := &fleet.Team{ID: 2}
runTestCases(t, []authTestCase{
{user: nil, object: team, action: read, allow: false},
{user: nil, object: team, action: write, allow: false},
{user: test.UserNoRoles, object: team, action: read, allow: true},
{user: test.UserNoRoles, object: team, action: write, allow: false},
{user: test.UserNoRoles, object: team1, action: read, allow: false},
{user: test.UserNoRoles, object: team1, action: write, allow: false},
{user: test.UserAdmin, object: team, action: read, allow: true},
{user: test.UserAdmin, object: team, action: write, allow: true},
{user: test.UserAdmin, object: team1, action: read, allow: true},
{user: test.UserAdmin, object: team1, action: write, allow: true},
{user: test.UserMaintainer, object: team, action: read, allow: true},
{user: test.UserMaintainer, object: team, action: write, allow: false},
{user: test.UserMaintainer, object: team1, action: read, allow: true},
{user: test.UserMaintainer, object: team1, action: write, allow: false},
{user: test.UserObserver, object: team, action: read, allow: true},
{user: test.UserObserver, object: team, action: write, allow: false},
{user: test.UserObserver, object: team1, action: read, allow: true},
{user: test.UserObserver, object: team1, action: write, allow: false},
{user: test.UserObserverPlus, object: team, action: read, allow: true},
{user: test.UserObserverPlus, object: team, action: write, allow: false},
{user: test.UserObserverPlus, object: team1, action: read, allow: true},
{user: test.UserObserverPlus, object: team1, action: write, allow: false},
{user: test.UserGitOps, object: team, action: read, allow: true},
{user: test.UserGitOps, object: team, action: write, allow: true},
{user: test.UserGitOps, object: team1, action: read, allow: false},
{user: test.UserGitOps, object: team1, action: write, allow: true},
{user: test.UserTeamAdminTeam1, object: team, action: read, allow: true},
{user: test.UserTeamAdminTeam1, object: team, action: write, allow: false},
{user: test.UserTeamAdminTeam1, object: team1, action: read, allow: true},
{user: test.UserTeamAdminTeam1, object: team1, action: write, allow: true},
{user: test.UserTeamAdminTeam1, object: team2, action: read, allow: false},
{user: test.UserTeamAdminTeam1, object: team2, action: write, allow: false},
{user: test.UserTeamMaintainerTeam1, object: team, action: read, allow: true},
{user: test.UserTeamMaintainerTeam1, object: team, action: write, allow: false},
{user: test.UserTeamMaintainerTeam1, object: team1, action: read, allow: true},
{user: test.UserTeamMaintainerTeam1, object: team1, action: write, allow: false},
{user: test.UserTeamMaintainerTeam1, object: team2, action: read, allow: false},
{user: test.UserTeamMaintainerTeam1, object: team2, action: write, allow: false},
{user: test.UserTeamObserverTeam1, object: team, action: read, allow: true},
{user: test.UserTeamObserverTeam1, object: team, action: write, allow: false},
{user: test.UserTeamObserverTeam1, object: team1, action: read, allow: true},
{user: test.UserTeamObserverTeam1, object: team1, action: write, allow: false},
{user: test.UserTeamObserverTeam1, object: team2, action: read, allow: false},
{user: test.UserTeamObserverTeam1, object: team2, action: write, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: team, action: read, allow: true},
{user: test.UserTeamObserverPlusTeam1, object: team, action: write, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: team1, action: read, allow: true},
{user: test.UserTeamObserverPlusTeam1, object: team1, action: write, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: team2, action: read, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: team2, action: write, allow: false},
{user: test.UserTeamGitOpsTeam1, object: team, action: read, allow: true},
{user: test.UserTeamGitOpsTeam1, object: team, action: write, allow: false},
// Team GitOps cannot read its team but can write it.
{user: test.UserTeamGitOpsTeam1, object: team1, action: read, allow: false},
{user: test.UserTeamGitOpsTeam1, object: team1, action: write, allow: true},
{user: test.UserTeamGitOpsTeam1, object: team2, action: read, allow: false},
{user: test.UserTeamGitOpsTeam1, object: team2, action: write, allow: false},
})
}
@ -338,7 +458,7 @@ func TestAuthorizeLabel(t *testing.T) {
{user: nil, object: label, action: read, allow: false},
{user: nil, object: label, action: write, allow: false},
{user: test.UserNoRoles, object: label, action: read, allow: true},
{user: test.UserNoRoles, object: label, action: read, allow: false},
{user: test.UserNoRoles, object: label, action: write, allow: false},
{user: test.UserAdmin, object: label, action: read, allow: true},
@ -352,6 +472,30 @@ func TestAuthorizeLabel(t *testing.T) {
{user: test.UserObserverPlus, object: label, action: read, allow: true},
{user: test.UserObserverPlus, object: label, action: write, allow: false},
// Global GitOps can write, but not read labels.
{user: test.UserGitOps, object: label, action: read, allow: false},
{user: test.UserGitOps, object: label, action: write, allow: true},
// Team GitOps cannot read or write labels.
{user: test.UserTeamGitOpsTeam1, object: label, action: read, allow: false},
{user: test.UserTeamGitOpsTeam1, object: label, action: write, allow: false},
})
}
func TestAuthorizeSoftwareInventory(t *testing.T) {
t.Parallel()
softwareInventory := &fleet.AuthzSoftwareInventory{}
runTestCases(t, []authTestCase{
{user: nil, object: softwareInventory, action: read, allow: false},
{user: test.UserNoRoles, object: softwareInventory, action: read, allow: false},
{user: test.UserAdmin, object: softwareInventory, action: read, allow: true},
{user: test.UserMaintainer, object: softwareInventory, action: read, allow: true},
{user: test.UserObserver, object: softwareInventory, action: read, allow: true},
{user: test.UserObserverPlus, object: softwareInventory, action: read, allow: true},
{user: test.UserGitOps, object: softwareInventory, action: read, allow: false},
{user: test.UserTeamGitOpsTeam1, object: softwareInventory, action: read, allow: false},
})
}
@ -373,6 +517,16 @@ func TestAuthorizeHost(t *testing.T) {
{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver},
},
}
teamObserverPlus := &fleet.User{
Teams: []fleet.UserTeam{
{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserverPlus},
},
}
teamGitOps := &fleet.User{
Teams: []fleet.UserTeam{
{Team: fleet.Team{ID: 1}, Role: fleet.RoleGitOps},
},
}
host := &fleet.Host{}
hostTeam1 := &fleet.Host{TeamID: ptr.Uint(1)}
hostTeam2 := &fleet.Host{TeamID: ptr.Uint(2)}
@ -389,10 +543,10 @@ func TestAuthorizeHost(t *testing.T) {
{user: nil, object: hostTeam2, action: write, allow: false},
{user: nil, object: hostTeam2, action: mdmCommand, allow: false},
// List but no specific host access
// No host access if the user has no roles.
{user: test.UserNoRoles, object: host, action: read, allow: false},
{user: test.UserNoRoles, object: host, action: write, allow: false},
{user: test.UserNoRoles, object: host, action: list, allow: true},
{user: test.UserNoRoles, object: host, action: list, allow: false},
{user: test.UserNoRoles, object: host, action: mdmCommand, allow: false},
{user: test.UserNoRoles, object: hostTeam1, action: read, allow: false},
{user: test.UserNoRoles, object: hostTeam1, action: write, allow: false},
@ -449,6 +603,18 @@ func TestAuthorizeHost(t *testing.T) {
{user: test.UserMaintainer, object: hostTeam2, action: write, allow: true},
{user: test.UserMaintainer, object: hostTeam2, action: mdmCommand, allow: true},
// Global GitOps can write (not read) all.
{user: test.UserGitOps, object: host, action: read, allow: false},
{user: test.UserGitOps, object: host, action: write, allow: true},
{user: test.UserGitOps, object: host, action: list, allow: false},
{user: test.UserGitOps, object: host, action: mdmCommand, allow: false},
{user: test.UserGitOps, object: hostTeam1, action: read, allow: false},
{user: test.UserGitOps, object: hostTeam1, action: write, allow: true},
{user: test.UserGitOps, object: hostTeam1, action: mdmCommand, allow: false},
{user: test.UserGitOps, object: hostTeam2, action: read, allow: false},
{user: test.UserGitOps, object: hostTeam2, action: write, allow: true},
{user: test.UserGitOps, object: hostTeam2, action: mdmCommand, allow: false},
// Team observer can read only on appropriate team
{user: teamObserver, object: host, action: read, allow: false},
{user: teamObserver, object: host, action: write, allow: false},
@ -461,6 +627,18 @@ func TestAuthorizeHost(t *testing.T) {
{user: teamObserver, object: hostTeam2, action: write, allow: false},
{user: teamObserver, object: hostTeam2, action: mdmCommand, allow: false},
// Team observer+ can read only on appropriate team
{user: teamObserverPlus, object: host, action: read, allow: false},
{user: teamObserverPlus, object: host, action: write, allow: false},
{user: teamObserverPlus, object: host, action: list, allow: true},
{user: teamObserverPlus, object: host, action: mdmCommand, allow: false},
{user: teamObserverPlus, object: hostTeam1, action: read, allow: true},
{user: teamObserverPlus, object: hostTeam1, action: write, allow: false},
{user: teamObserverPlus, object: hostTeam1, action: mdmCommand, allow: false},
{user: teamObserverPlus, object: hostTeam2, action: read, allow: false},
{user: teamObserverPlus, object: hostTeam2, action: write, allow: false},
{user: teamObserverPlus, object: hostTeam2, action: mdmCommand, allow: false},
// Team maintainer can read/write only on appropriate team
{user: teamMaintainer, object: host, action: read, allow: false},
{user: teamMaintainer, object: host, action: write, allow: false},
@ -484,6 +662,18 @@ func TestAuthorizeHost(t *testing.T) {
{user: teamAdmin, object: hostTeam2, action: read, allow: false},
{user: teamAdmin, object: hostTeam2, action: write, allow: false},
{user: teamAdmin, object: hostTeam2, action: mdmCommand, allow: false},
// Team GitOps can cannot read/write hosts.
{user: teamGitOps, object: host, action: read, allow: false},
{user: teamGitOps, object: host, action: write, allow: false},
{user: teamGitOps, object: host, action: list, allow: false},
{user: teamGitOps, object: host, action: mdmCommand, allow: false},
{user: teamGitOps, object: hostTeam1, action: read, allow: false},
{user: teamGitOps, object: hostTeam1, action: write, allow: false},
{user: teamGitOps, object: hostTeam1, action: mdmCommand, allow: false},
{user: teamGitOps, object: hostTeam2, action: read, allow: false},
{user: teamGitOps, object: hostTeam2, action: write, allow: false},
{user: teamGitOps, object: hostTeam2, action: mdmCommand, allow: false},
})
}
@ -521,6 +711,12 @@ func TestAuthorizeQuery(t *testing.T) {
{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserverPlus},
},
}
teamGitOps := &fleet.User{
ID: 105,
Teams: []fleet.UserTeam{
{Team: fleet.Team{ID: 1}, Role: fleet.RoleGitOps},
},
}
query := &fleet.Query{ObserverCanRun: false}
emptyTquery := &fleet.TargetedQuery{Query: query}
@ -539,6 +735,8 @@ func TestAuthorizeQuery(t *testing.T) {
teamAdminQuery := &fleet.Query{ID: 1, AuthorID: ptr.Uint(teamAdmin.ID), ObserverCanRun: false}
teamMaintQuery := &fleet.Query{ID: 2, AuthorID: ptr.Uint(teamMaintainer.ID), ObserverCanRun: false}
globalAdminQuery := &fleet.Query{ID: 3, AuthorID: ptr.Uint(test.UserAdmin.ID), ObserverCanRun: false}
globalGitOpsQuery := &fleet.Query{ID: 4, AuthorID: ptr.Uint(test.UserGitOps.ID), ObserverCanRun: false}
teamGitOpsQuery := &fleet.Query{ID: 5, AuthorID: ptr.Uint(teamGitOps.ID), ObserverCanRun: false}
runTestCases(t, []authTestCase{
// No access
@ -554,14 +752,14 @@ func TestAuthorizeQuery(t *testing.T) {
{user: nil, object: team1ObsQuery, action: run, allow: false},
{user: nil, object: observerQuery, action: runNew, allow: false},
// User can still read queries with no roles
{user: test.UserNoRoles, object: query, action: read, allow: true},
// User with no roles cannot access queries.
{user: test.UserNoRoles, object: query, action: read, allow: false},
{user: test.UserNoRoles, object: query, action: write, allow: false},
{user: test.UserNoRoles, object: teamAdminQuery, action: write, allow: false},
{user: test.UserNoRoles, object: emptyTquery, action: run, allow: false},
{user: test.UserNoRoles, object: team1Query, action: run, allow: false},
{user: test.UserNoRoles, object: query, action: runNew, allow: false},
{user: test.UserNoRoles, object: observerQuery, action: read, allow: true},
{user: test.UserNoRoles, object: observerQuery, action: read, allow: false},
{user: test.UserNoRoles, object: observerQuery, action: write, allow: false},
{user: test.UserNoRoles, object: emptyTobsQuery, action: run, allow: false},
{user: test.UserNoRoles, object: team1ObsQuery, action: run, allow: false},
@ -623,6 +821,20 @@ func TestAuthorizeQuery(t *testing.T) {
{user: test.UserAdmin, object: team1ObsQuery, action: run, allow: true},
{user: test.UserAdmin, object: observerQuery, action: runNew, allow: true},
// Global GitOps cannot read, or run any query, but can write.
{user: test.UserGitOps, object: query, action: read, allow: false},
{user: test.UserGitOps, object: query, action: write, allow: true},
{user: test.UserGitOps, object: teamAdminQuery, action: write, allow: true},
{user: test.UserGitOps, object: emptyTquery, action: run, allow: false},
{user: test.UserGitOps, object: team1Query, action: run, allow: false},
{user: test.UserGitOps, object: query, action: runNew, allow: false},
{user: test.UserGitOps, object: observerQuery, action: read, allow: false},
{user: test.UserGitOps, object: observerQuery, action: write, allow: true},
{user: test.UserGitOps, object: emptyTobsQuery, action: run, allow: false},
{user: test.UserGitOps, object: team1ObsQuery, action: run, allow: false},
{user: test.UserGitOps, object: team12ObsQuery, action: run, allow: false},
{user: test.UserGitOps, object: observerQuery, action: runNew, allow: false},
// Team observer can read and run observer_can_run only
{user: teamObserver, object: query, action: read, allow: true},
{user: teamObserver, object: query, action: write, allow: false},
@ -690,6 +902,21 @@ func TestAuthorizeQuery(t *testing.T) {
{user: teamAdmin, object: team2ObsQuery, action: run, allow: false},
{user: teamAdmin, object: observerQuery, action: runNew, allow: true},
// Team GitOps cannot read or run any query, but can create new or edit (write) queries authored by it.
{user: teamGitOps, object: query, action: read, allow: false},
{user: teamGitOps, object: query, action: write, allow: true}, // create new
{user: teamGitOps, object: teamAdminQuery, action: write, allow: false}, // not the author
{user: teamGitOps, object: teamGitOpsQuery, action: write, allow: true}, // author
{user: teamGitOps, object: globalGitOpsQuery, action: write, allow: false}, // not the author
{user: teamGitOps, object: emptyTquery, action: run, allow: false},
{user: teamGitOps, object: team1Query, action: run, allow: false},
{user: teamGitOps, object: query, action: runNew, allow: false},
{user: teamGitOps, object: emptyTobsQuery, action: run, allow: false},
{user: teamGitOps, object: team1ObsQuery, action: run, allow: false},
{user: teamGitOps, object: team12ObsQuery, action: run, allow: false},
{user: teamGitOps, object: team2ObsQuery, action: run, allow: false},
{user: teamGitOps, object: observerQuery, action: runNew, allow: false},
// User admin on team 1, observer on team 2
{user: twoTeamsAdminObs, object: query, action: read, allow: true},
{user: twoTeamsAdminObs, object: query, action: write, allow: true},
@ -713,7 +940,7 @@ func TestAuthorizeQuery(t *testing.T) {
})
}
func TestAuthorizeTargets(t *testing.T) {
func TestAuthorizeTarget(t *testing.T) {
t.Parallel()
target := &fleet.Target{}
@ -730,105 +957,158 @@ func TestAuthorizeTargets(t *testing.T) {
})
}
func TestAuthorizePacks(t *testing.T) {
func TestAuthorizeUserCreatedPack(t *testing.T) {
t.Parallel()
pack := &fleet.Pack{}
userCreatedPack := &fleet.Pack{
// Type nil is the type for user-created packs.
Type: nil,
}
runTestCases(t, []authTestCase{
{user: nil, object: pack, action: read, allow: false},
{user: nil, object: pack, action: write, allow: false},
{user: nil, object: userCreatedPack, action: read, allow: false},
{user: nil, object: userCreatedPack, action: write, allow: false},
{user: test.UserNoRoles, object: pack, action: read, allow: false},
{user: test.UserNoRoles, object: pack, action: write, allow: false},
{user: test.UserNoRoles, object: userCreatedPack, action: read, allow: false},
{user: test.UserNoRoles, object: userCreatedPack, action: write, allow: false},
{user: test.UserAdmin, object: pack, action: read, allow: true},
{user: test.UserAdmin, object: pack, action: write, allow: true},
{user: test.UserAdmin, object: userCreatedPack, action: read, allow: true},
{user: test.UserAdmin, object: userCreatedPack, action: write, allow: true},
{user: test.UserMaintainer, object: pack, action: read, allow: true},
{user: test.UserMaintainer, object: pack, action: write, allow: true},
{user: test.UserMaintainer, object: userCreatedPack, action: read, allow: true},
{user: test.UserMaintainer, object: userCreatedPack, action: write, allow: true},
{user: test.UserObserver, object: pack, action: read, allow: false},
{user: test.UserObserver, object: pack, action: write, allow: false},
{user: test.UserObserver, object: userCreatedPack, action: read, allow: false},
{user: test.UserObserver, object: userCreatedPack, action: write, allow: false},
{user: test.UserObserverPlus, object: pack, action: read, allow: false},
{user: test.UserObserverPlus, object: pack, action: write, allow: false},
{user: test.UserObserverPlus, object: userCreatedPack, action: read, allow: false},
{user: test.UserObserverPlus, object: userCreatedPack, action: write, allow: false},
// This is one exception to the "write only" nature of gitops. To be able to create
// and edit packs currently it needs read access too.
{user: test.UserGitOps, object: userCreatedPack, action: read, allow: true},
{user: test.UserGitOps, object: userCreatedPack, action: write, allow: true},
{user: test.UserTeamAdminTeam1, object: userCreatedPack, action: read, allow: false},
{user: test.UserTeamAdminTeam1, object: userCreatedPack, action: write, allow: false},
{user: test.UserTeamMaintainerTeam1, object: userCreatedPack, action: read, allow: false},
{user: test.UserTeamMaintainerTeam1, object: userCreatedPack, action: write, allow: false},
{user: test.UserTeamObserverTeam1, object: userCreatedPack, action: read, allow: false},
{user: test.UserTeamObserverTeam1, object: userCreatedPack, action: write, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: userCreatedPack, action: read, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: userCreatedPack, action: write, allow: false},
{user: test.UserTeamGitOpsTeam1, object: userCreatedPack, action: read, allow: false},
{user: test.UserTeamGitOpsTeam1, object: userCreatedPack, action: write, allow: false},
})
}
func TestAuthorizeTeamPacks(t *testing.T) {
func TestAuthorizeGlobalPack(t *testing.T) {
t.Parallel()
globalPack := &fleet.Pack{
// Type "global" is the type for the one global pack.
Type: ptr.String("global"),
}
runTestCases(t, []authTestCase{
// Team maintainer can read packs of the team.
{
user: test.UserTeamMaintainerTeam1,
object: &fleet.Pack{
Type: ptr.String("team-1"),
},
action: read,
allow: true,
},
// Team observer can read packs of the team.
{
user: test.UserTeamObserverTeam1TeamAdminTeam2,
object: &fleet.Pack{
Type: ptr.String("team-1"),
},
action: read,
allow: true,
},
// Team observer cannot write packs of the team.
{
user: test.UserTeamObserverTeam1TeamAdminTeam2,
object: &fleet.Pack{
Type: ptr.String("team-1"),
},
action: write,
allow: false,
},
// Members of a team cannot read packs of another team.
{
user: test.UserTeamAdminTeam1,
object: &fleet.Pack{
Type: ptr.String("team-2"),
},
action: read,
allow: false,
},
// Members of a team cannot read packs of another team.
{
user: test.UserTeamAdminTeam1,
object: &fleet.Pack{
Type: ptr.String("team-2"),
},
action: read,
allow: false,
},
// Team maintainers cannot read global packs.
{
user: test.UserTeamMaintainerTeam1,
object: &fleet.Pack{},
action: read,
allow: false,
},
// Team admins cannot read global packs.
{
user: test.UserTeamAdminTeam1,
object: &fleet.Pack{},
action: read,
allow: false,
},
// Team admins cannot write global packs.
{
user: test.UserTeamAdminTeam1,
object: &fleet.Pack{},
action: write,
allow: false,
},
{user: nil, object: globalPack, action: read, allow: false},
{user: nil, object: globalPack, action: write, allow: false},
{user: test.UserNoRoles, object: globalPack, action: read, allow: false},
{user: test.UserNoRoles, object: globalPack, action: write, allow: false},
{user: test.UserAdmin, object: globalPack, action: read, allow: true},
{user: test.UserAdmin, object: globalPack, action: write, allow: true},
{user: test.UserMaintainer, object: globalPack, action: read, allow: true},
{user: test.UserMaintainer, object: globalPack, action: write, allow: true},
{user: test.UserObserver, object: globalPack, action: read, allow: true},
{user: test.UserObserver, object: globalPack, action: write, allow: false},
{user: test.UserObserverPlus, object: globalPack, action: read, allow: true},
{user: test.UserObserverPlus, object: globalPack, action: write, allow: false},
// This is one exception to the "write only" nature of gitops. To be able to create
// and edit packs currently it needs read access too.
{user: test.UserGitOps, object: globalPack, action: read, allow: true},
{user: test.UserGitOps, object: globalPack, action: write, allow: true},
{user: test.UserTeamAdminTeam1, object: globalPack, action: read, allow: true},
{user: test.UserTeamAdminTeam1, object: globalPack, action: write, allow: false},
{user: test.UserTeamMaintainerTeam1, object: globalPack, action: read, allow: true},
{user: test.UserTeamMaintainerTeam1, object: globalPack, action: write, allow: false},
{user: test.UserTeamObserverTeam1, object: globalPack, action: read, allow: true},
{user: test.UserTeamObserverTeam1, object: globalPack, action: write, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: globalPack, action: read, allow: true},
{user: test.UserTeamObserverPlusTeam1, object: globalPack, action: write, allow: false},
{user: test.UserTeamGitOpsTeam1, object: globalPack, action: read, allow: false},
{user: test.UserTeamGitOpsTeam1, object: globalPack, action: write, allow: false},
})
}
func TestAuthorizeCarves(t *testing.T) {
func TestAuthorizeTeamPack(t *testing.T) {
t.Parallel()
team1Pack := &fleet.Pack{Type: ptr.String("team-1")}
team2Pack := &fleet.Pack{Type: ptr.String("team-2")}
runTestCases(t, []authTestCase{
{user: test.UserAdmin, object: team1Pack, action: read, allow: true},
{user: test.UserAdmin, object: team1Pack, action: write, allow: true},
{user: test.UserMaintainer, object: team1Pack, action: read, allow: true},
{user: test.UserMaintainer, object: team1Pack, action: write, allow: true},
{user: test.UserObserver, object: team1Pack, action: read, allow: false},
{user: test.UserObserver, object: team1Pack, action: write, allow: false},
{user: test.UserObserverPlus, object: team1Pack, action: read, allow: false},
{user: test.UserObserverPlus, object: team1Pack, action: write, allow: false},
// This is one exception to the "write only" nature of gitops. To be able to create
// and edit packs currently it needs read access too.
{user: test.UserGitOps, object: team1Pack, action: read, allow: true},
{user: test.UserGitOps, object: team1Pack, action: write, allow: true},
{user: test.UserTeamAdminTeam1, object: team1Pack, action: read, allow: true},
{user: test.UserTeamAdminTeam1, object: team1Pack, action: write, allow: true},
{user: test.UserTeamMaintainerTeam1, object: team1Pack, action: read, allow: true},
{user: test.UserTeamMaintainerTeam1, object: team1Pack, action: read, allow: true},
{user: test.UserTeamObserverTeam1, object: team1Pack, action: read, allow: true},
{user: test.UserTeamObserverTeam1, object: team1Pack, action: write, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: team1Pack, action: read, allow: true},
{user: test.UserTeamObserverPlusTeam1, object: team1Pack, action: write, allow: false},
{user: test.UserTeamGitOpsTeam1, object: team1Pack, action: read, allow: false},
{user: test.UserTeamGitOpsTeam1, object: team1Pack, action: write, allow: true},
{user: test.UserTeamAdminTeam1, object: team2Pack, action: read, allow: false},
{user: test.UserTeamAdminTeam1, object: team2Pack, action: write, allow: false},
{user: test.UserTeamMaintainerTeam1, object: team2Pack, action: read, allow: false},
{user: test.UserTeamMaintainerTeam1, object: team2Pack, action: write, allow: false},
{user: test.UserTeamObserverTeam1, object: team2Pack, action: read, allow: false},
{user: test.UserTeamObserverTeam1, object: team2Pack, action: write, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: team2Pack, action: read, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: team2Pack, action: write, allow: false},
{user: test.UserTeamGitOpsTeam1, object: team2Pack, action: read, allow: false},
{user: test.UserTeamGitOpsTeam1, object: team2Pack, action: write, allow: false},
})
}
func TestAuthorizeCarve(t *testing.T) {
t.Parallel()
carve := &fleet.CarveMetadata{}
@ -843,6 +1123,8 @@ func TestAuthorizeCarves(t *testing.T) {
{user: test.UserObserver, object: carve, action: write, allow: false},
{user: test.UserObserverPlus, object: carve, action: read, allow: false},
{user: test.UserObserverPlus, object: carve, action: write, allow: false},
{user: test.UserGitOps, object: carve, action: read, allow: false},
{user: test.UserGitOps, object: carve, action: write, allow: false},
// Only admins allowed
{user: test.UserAdmin, object: carve, action: read, allow: true},
@ -850,58 +1132,113 @@ func TestAuthorizeCarves(t *testing.T) {
})
}
func TestAuthorizePolicies(t *testing.T) {
func TestAuthorizeGlobalPolicy(t *testing.T) {
t.Parallel()
globalPolicy := &fleet.Policy{}
runTestCases(t, []authTestCase{
{user: nil, object: globalPolicy, action: write, allow: false},
{user: nil, object: globalPolicy, action: read, allow: false},
{user: test.UserNoRoles, object: globalPolicy, action: write, allow: false},
{user: test.UserNoRoles, object: globalPolicy, action: read, allow: false},
{user: test.UserAdmin, object: globalPolicy, action: write, allow: true},
{user: test.UserAdmin, object: globalPolicy, action: read, allow: true},
{user: test.UserMaintainer, object: globalPolicy, action: write, allow: true},
{user: test.UserMaintainer, object: globalPolicy, action: read, allow: true},
{user: test.UserObserver, object: globalPolicy, action: write, allow: false},
{user: test.UserObserver, object: globalPolicy, action: read, allow: true},
{user: test.UserObserverPlus, object: globalPolicy, action: write, allow: false},
{user: test.UserObserverPlus, object: globalPolicy, action: read, allow: true},
{user: test.UserGitOps, object: globalPolicy, action: write, allow: true},
{user: test.UserGitOps, object: globalPolicy, action: read, allow: false},
{user: test.UserTeamAdminTeam1, object: globalPolicy, action: write, allow: false},
{user: test.UserTeamAdminTeam1, object: globalPolicy, action: read, allow: true},
{user: test.UserTeamMaintainerTeam1, object: globalPolicy, action: write, allow: false},
{user: test.UserTeamMaintainerTeam1, object: globalPolicy, action: read, allow: true},
{user: test.UserTeamObserverTeam1, object: globalPolicy, action: write, allow: false},
{user: test.UserTeamObserverTeam1, object: globalPolicy, action: read, allow: true},
{user: test.UserTeamObserverPlusTeam1, object: globalPolicy, action: write, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: globalPolicy, action: read, allow: true},
{user: test.UserTeamGitOpsTeam1, object: globalPolicy, action: write, allow: false},
{user: test.UserTeamGitOpsTeam1, object: globalPolicy, action: read, allow: false},
})
}
func TestAuthorizeTeamPolicy(t *testing.T) {
t.Parallel()
team1Policy := &fleet.Policy{
PolicyData: fleet.PolicyData{
TeamID: ptr.Uint(1),
},
}
team2Policy := &fleet.Policy{
PolicyData: fleet.PolicyData{
TeamID: ptr.Uint(2),
},
}
runTestCases(t, []authTestCase{
{user: test.UserNoRoles, object: globalPolicy, action: write, allow: false},
{user: nil, object: team1Policy, action: write, allow: false},
{user: nil, object: team1Policy, action: read, allow: false},
{user: test.UserAdmin, object: globalPolicy, action: write, allow: true},
{user: test.UserAdmin, object: globalPolicy, action: read, allow: true},
{user: test.UserMaintainer, object: globalPolicy, action: write, allow: true},
{user: test.UserMaintainer, object: globalPolicy, action: read, allow: true},
{user: test.UserObserver, object: globalPolicy, action: write, allow: false},
{user: test.UserObserver, object: globalPolicy, action: read, allow: true},
{user: test.UserNoRoles, object: team1Policy, action: write, allow: false},
{user: test.UserNoRoles, object: team1Policy, action: read, allow: false},
{user: test.UserAdmin, object: team1Policy, action: write, allow: true},
{user: test.UserAdmin, object: team1Policy, action: read, allow: true},
{user: test.UserMaintainer, object: team1Policy, action: write, allow: true},
{user: test.UserMaintainer, object: team1Policy, action: read, allow: true},
{user: test.UserObserver, object: team1Policy, action: write, allow: false},
{user: test.UserObserver, object: team1Policy, action: read, allow: true},
{user: test.UserObserverPlus, object: team1Policy, action: write, allow: false},
{user: test.UserObserverPlus, object: team1Policy, action: read, allow: true},
{user: test.UserGitOps, object: team1Policy, action: write, allow: true},
{user: test.UserGitOps, object: team1Policy, action: read, allow: false},
{user: test.UserTeamAdminTeam1, object: team1Policy, action: write, allow: true},
{user: test.UserTeamAdminTeam1, object: team1Policy, action: read, allow: true},
{user: test.UserTeamAdminTeam2, object: team1Policy, action: write, allow: false},
{user: test.UserTeamAdminTeam2, object: team1Policy, action: read, allow: false},
{user: test.UserTeamMaintainerTeam1, object: team1Policy, action: write, allow: true},
{user: test.UserTeamMaintainerTeam1, object: team1Policy, action: read, allow: true},
{user: test.UserTeamMaintainerTeam2, object: team1Policy, action: write, allow: false},
{user: test.UserTeamMaintainerTeam2, object: team1Policy, action: read, allow: false},
{user: test.UserTeamObserverTeam1, object: team1Policy, action: write, allow: false},
{user: test.UserTeamObserverTeam1, object: team1Policy, action: read, allow: true},
{user: test.UserTeamObserverTeam2, object: team1Policy, action: write, allow: false},
{user: test.UserTeamObserverTeam2, object: team1Policy, action: read, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: team1Policy, action: write, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: team1Policy, action: read, allow: true},
{user: test.UserTeamObserverPlusTeam2, object: team1Policy, action: write, allow: false},
{user: test.UserTeamObserverPlusTeam2, object: team1Policy, action: read, allow: false},
// Team observers cannot write global policies.
{user: test.UserTeamObserverTeam1, object: globalPolicy, action: write, allow: false},
// Team observers can read global policies.
{user: test.UserTeamObserverTeam1, object: globalPolicy, action: read, allow: true},
{user: test.UserTeamGitOpsTeam1, object: team1Policy, action: write, allow: true},
{user: test.UserTeamGitOpsTeam1, object: team1Policy, action: read, allow: false},
{user: test.UserTeamAdminTeam1, object: team2Policy, action: write, allow: false},
{user: test.UserTeamAdminTeam1, object: team2Policy, action: read, allow: false},
{user: test.UserTeamMaintainerTeam1, object: team2Policy, action: write, allow: false},
{user: test.UserTeamMaintainerTeam1, object: team2Policy, action: read, allow: false},
{user: test.UserTeamObserverTeam1, object: team2Policy, action: write, allow: false},
{user: test.UserTeamObserverTeam1, object: team2Policy, action: read, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: team2Policy, action: write, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: team2Policy, action: read, allow: false},
{user: test.UserTeamGitOpsTeam1, object: team2Policy, action: write, allow: false},
{user: test.UserTeamGitOpsTeam1, object: team2Policy, action: read, allow: false},
})
}
@ -938,6 +1275,11 @@ func TestAuthorizeMDMAppleConfigProfile(t *testing.T) {
{user: test.UserObserverPlus, object: team1Profile, action: write, allow: false},
{user: test.UserObserverPlus, object: team1Profile, action: read, allow: false},
{user: test.UserGitOps, object: globalProfile, action: write, allow: false},
{user: test.UserGitOps, object: globalProfile, action: read, allow: false},
{user: test.UserGitOps, object: team1Profile, action: write, allow: false},
{user: test.UserGitOps, object: team1Profile, action: read, allow: false},
{user: test.UserTeamAdminTeam1, object: globalProfile, action: write, allow: false},
{user: test.UserTeamAdminTeam1, object: globalProfile, action: read, allow: false},
{user: test.UserTeamAdminTeam1, object: team1Profile, action: write, allow: true},
@ -963,20 +1305,30 @@ func TestAuthorizeMDMAppleConfigProfile(t *testing.T) {
{user: test.UserTeamObserverTeam1, object: team1Profile, action: write, allow: false},
{user: test.UserTeamObserverTeam1, object: team1Profile, action: read, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: globalProfile, action: write, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: globalProfile, action: read, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: team1Profile, action: write, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: team1Profile, action: read, allow: false},
{user: test.UserTeamObserverTeam2, object: globalProfile, action: write, allow: false},
{user: test.UserTeamObserverTeam2, object: globalProfile, action: read, allow: false},
{user: test.UserTeamObserverTeam2, object: team1Profile, action: write, allow: false},
{user: test.UserTeamObserverTeam2, object: team1Profile, action: read, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: globalProfile, action: write, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: globalProfile, action: read, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: team1Profile, action: write, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: team1Profile, action: read, allow: false},
{user: test.UserTeamObserverPlusTeam2, object: globalProfile, action: write, allow: false},
{user: test.UserTeamObserverPlusTeam2, object: globalProfile, action: read, allow: false},
{user: test.UserTeamObserverPlusTeam2, object: team1Profile, action: write, allow: false},
{user: test.UserTeamObserverPlusTeam2, object: team1Profile, action: read, allow: false},
{user: test.UserTeamGitOpsTeam1, object: globalProfile, action: write, allow: false},
{user: test.UserTeamGitOpsTeam1, object: globalProfile, action: read, allow: false},
{user: test.UserTeamGitOpsTeam1, object: team1Profile, action: write, allow: false},
{user: test.UserTeamGitOpsTeam1, object: team1Profile, action: read, allow: false},
{user: test.UserTeamGitOpsTeam2, object: globalProfile, action: write, allow: false},
{user: test.UserTeamGitOpsTeam2, object: globalProfile, action: read, allow: false},
{user: test.UserTeamGitOpsTeam2, object: team1Profile, action: write, allow: false},
{user: test.UserTeamGitOpsTeam2, object: team1Profile, action: read, allow: false},
})
}
@ -1013,6 +1365,11 @@ func TestAuthorizeMDMAppleSettings(t *testing.T) {
{user: test.UserObserverPlus, object: team1Settings, action: write, allow: false},
{user: test.UserObserverPlus, object: team1Settings, action: read, allow: false},
{user: test.UserGitOps, object: globalSettings, action: write, allow: false},
{user: test.UserGitOps, object: globalSettings, action: read, allow: false},
{user: test.UserGitOps, object: team1Settings, action: write, allow: false},
{user: test.UserGitOps, object: team1Settings, action: read, allow: false},
{user: test.UserTeamAdminTeam1, object: globalSettings, action: write, allow: false},
{user: test.UserTeamAdminTeam1, object: globalSettings, action: read, allow: false},
{user: test.UserTeamAdminTeam1, object: team1Settings, action: write, allow: true},
@ -1038,20 +1395,30 @@ func TestAuthorizeMDMAppleSettings(t *testing.T) {
{user: test.UserTeamObserverTeam1, object: team1Settings, action: write, allow: false},
{user: test.UserTeamObserverTeam1, object: team1Settings, action: read, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: globalSettings, action: write, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: globalSettings, action: read, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: team1Settings, action: write, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: team1Settings, action: read, allow: false},
{user: test.UserTeamObserverTeam2, object: globalSettings, action: write, allow: false},
{user: test.UserTeamObserverTeam2, object: globalSettings, action: read, allow: false},
{user: test.UserTeamObserverTeam2, object: team1Settings, action: write, allow: false},
{user: test.UserTeamObserverTeam2, object: team1Settings, action: read, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: globalSettings, action: write, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: globalSettings, action: read, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: team1Settings, action: write, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: team1Settings, action: read, allow: false},
{user: test.UserTeamObserverPlusTeam2, object: globalSettings, action: write, allow: false},
{user: test.UserTeamObserverPlusTeam2, object: globalSettings, action: read, allow: false},
{user: test.UserTeamObserverPlusTeam2, object: team1Settings, action: write, allow: false},
{user: test.UserTeamObserverPlusTeam2, object: team1Settings, action: read, allow: false},
{user: test.UserTeamGitOpsTeam1, object: globalSettings, action: write, allow: false},
{user: test.UserTeamGitOpsTeam1, object: globalSettings, action: read, allow: false},
{user: test.UserTeamGitOpsTeam1, object: team1Settings, action: write, allow: false},
{user: test.UserTeamGitOpsTeam1, object: team1Settings, action: read, allow: false},
{user: test.UserTeamGitOpsTeam2, object: globalSettings, action: write, allow: false},
{user: test.UserTeamGitOpsTeam2, object: globalSettings, action: read, allow: false},
{user: test.UserTeamGitOpsTeam2, object: team1Settings, action: write, allow: false},
{user: test.UserTeamGitOpsTeam2, object: team1Settings, action: read, allow: false},
})
}

View File

@ -277,6 +277,19 @@ type AppConfig struct {
/////////////////////////////////////////////////////////////////
}
// Obfuscate overrides credentials with obfuscated characters.
func (c *AppConfig) Obfuscate() {
if c.SMTPSettings.SMTPPassword != "" {
c.SMTPSettings.SMTPPassword = MaskedPassword
}
for _, jiraIntegration := range c.Integrations.Jira {
jiraIntegration.APIToken = MaskedPassword
}
for _, zdIntegration := range c.Integrations.Zendesk {
zdIntegration.APIToken = MaskedPassword
}
}
// legacyConfig holds settings that have been replaced, superceded or
// deprecated by other AppConfig settings.
type legacyConfig struct {
@ -678,6 +691,19 @@ func (e *EnrollSecret) AuthzType() string {
return "enroll_secret"
}
// ExtraAuthz implements authz.ExtraAuthzer.
func (e *EnrollSecret) ExtraAuthz() (map[string]interface{}, error) {
return map[string]interface{}{
"is_global_secret": e.TeamID == nil,
}, nil
}
// IsGlobalSecret returns whether the secret is global.
// This method is defined for the Policy Rego code (is_global_secret).
func (e *EnrollSecret) IsGlobalSecret() bool {
return e.TeamID == nil
}
const (
EnrollSecretKind = "enroll_secret"
EnrollSecretDefaultLength = 24
@ -835,3 +861,11 @@ type DeviceGlobalConfig struct {
type DeviceGlobalMDMConfig struct {
EnabledAndConfigured bool `json:"enabled_and_configured"`
}
// Version is the authz type used to check access control to the version endpoint.
type Version struct{}
// AuthzType implements authz.AuthzTyper.
func (v *Version) AuthzType() string {
return "version"
}

View File

@ -321,6 +321,8 @@ const (
ErrNoOneAdminNeeded = 2
// ErrNoUnknownTranslate is returned when an item type in the translate payload is unknown
ErrNoUnknownTranslate = 3
// ErrAPIOnlyRole is returned when a selected role for a user is for API only users.
ErrAPIOnlyRole = 4
)
// NewError returns a fleet error with the code and message specified

View File

@ -12,6 +12,7 @@ const (
RoleMaintainer = "maintainer"
RoleObserver = "observer"
RoleObserverPlus = "observer_plus"
RoleGitOps = "gitops"
)
type TeamPayload struct {
@ -204,10 +205,12 @@ var teamRoles = map[string]struct{}{
RoleObserver: {},
RoleMaintainer: {},
RoleObserverPlus: {},
RoleGitOps: {},
}
var premiumTeamRoles = map[string]struct{}{
RoleObserverPlus: {},
RoleGitOps: {},
}
// ValidTeamRole returns whether the role provided is valid for a team user.
@ -216,24 +219,17 @@ func ValidTeamRole(role string) bool {
return ok
}
// ValidTeamRoles returns the list of valid roles for a team user.
func ValidTeamRoles() []string {
var roles []string
for role := range teamRoles {
roles = append(roles, role)
}
return roles
}
var globalRoles = map[string]struct{}{
RoleObserver: {},
RoleMaintainer: {},
RoleAdmin: {},
RoleObserverPlus: {},
RoleGitOps: {},
}
var premiumGlobalRoles = map[string]struct{}{
RoleObserverPlus: {},
RoleGitOps: {},
}
// ValidGlobalRole returns whether the role provided is valid for a global user.
@ -242,15 +238,6 @@ func ValidGlobalRole(role string) bool {
return ok
}
// ValidGlobalRoles returns the list of valid roles for a global user.
func ValidGlobalRoles() []string {
var roles []string
for role := range globalRoles {
roles = append(roles, role)
}
return roles
}
// ValidateRole returns nil if the global and team roles combination is a valid
// one within fleet, or a fleet Error otherwise.
func ValidateRole(globalRole *string, teamUsers []UserTeam) error {
@ -277,21 +264,31 @@ func ValidateRole(globalRole *string, teamUsers []UserTeam) error {
return nil
}
func ValidateRoleForLicense(globalRole *string, teamUsers *[]UserTeam, license LicenseInfo) error {
// ValidateUserRoles verifies the roles to be applied to a new or existing user.
//
// Argument createNew sets whether the user is being created (true) or is being modified (false).
func ValidateUserRoles(createNew bool, payload UserPayload, license LicenseInfo) error {
var teamUsers_ []UserTeam
if teamUsers != nil {
teamUsers_ = *teamUsers
if payload.Teams != nil {
teamUsers_ = *payload.Teams
}
if err := ValidateRole(globalRole, teamUsers_); err != nil {
if err := ValidateRole(payload.GlobalRole, teamUsers_); err != nil {
return err
}
premiumRolesPresent := false
if globalRole != nil {
if _, ok := premiumGlobalRoles[*globalRole]; ok {
gitOpsRolePresent := false
if payload.GlobalRole != nil {
if *payload.GlobalRole == RoleGitOps {
gitOpsRolePresent = true
}
if _, ok := premiumGlobalRoles[*payload.GlobalRole]; ok {
premiumRolesPresent = true
}
}
for _, teamUser := range teamUsers_ {
if teamUser.Role == RoleGitOps {
gitOpsRolePresent = true
}
if _, ok := premiumTeamRoles[teamUser.Role]; ok {
premiumRolesPresent = true
}
@ -299,6 +296,14 @@ func ValidateRoleForLicense(globalRole *string, teamUsers *[]UserTeam, license L
if !license.IsPremium() && premiumRolesPresent {
return ErrMissingLicense
}
if gitOpsRolePresent &&
// New user is not API only.
((createNew && (payload.APIOnly == nil || !*payload.APIOnly)) ||
// Removing API only status from existing user.
(!createNew && payload.APIOnly != nil && !*payload.APIOnly)) {
return NewErrorf(ErrAPIOnlyRole, "role GitOps can only be set for API only users")
}
return nil
}

260
server/fleet/teams_test.go Normal file
View File

@ -0,0 +1,260 @@
package fleet
import (
"testing"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/stretchr/testify/require"
)
func TestValidateUserRoles(t *testing.T) {
checkErrCode := func(code int) func(error) bool {
return func(err error) bool {
errError, ok := err.(*Error)
if !ok {
return false
}
return errError.Code == code
}
}
for _, tc := range []struct {
name string
create bool
payload UserPayload
license LicenseInfo
checkErr func(err error) bool
}{
{
name: "global-gitops-create-not-premium",
create: true,
payload: UserPayload{
GlobalRole: ptr.String(RoleGitOps),
APIOnly: ptr.Bool(true),
},
license: LicenseInfo{
Tier: TierFree,
},
checkErr: func(err error) bool {
return err == ErrMissingLicense
},
},
{
name: "global-gitops-create-api-only",
create: true,
payload: UserPayload{
GlobalRole: ptr.String(RoleGitOps),
APIOnly: ptr.Bool(true),
},
license: LicenseInfo{
Tier: TierPremium,
},
checkErr: nil,
},
{
name: "global-gitops-create-not-api-only",
create: true,
payload: UserPayload{
GlobalRole: ptr.String(RoleGitOps),
APIOnly: ptr.Bool(false),
},
license: LicenseInfo{
Tier: TierPremium,
},
checkErr: checkErrCode(ErrAPIOnlyRole),
},
{
name: "global-gitops-create-api-only-not-set",
create: true,
payload: UserPayload{
GlobalRole: ptr.String(RoleGitOps),
APIOnly: nil,
},
license: LicenseInfo{
Tier: TierPremium,
},
checkErr: checkErrCode(ErrAPIOnlyRole),
},
{
name: "global-gitops-create-api-only-not-set",
create: true,
payload: UserPayload{
GlobalRole: ptr.String(RoleGitOps),
APIOnly: nil,
},
license: LicenseInfo{
Tier: TierPremium,
},
checkErr: checkErrCode(ErrAPIOnlyRole),
},
{
name: "global-gitops-modify-not-api-only",
create: false,
payload: UserPayload{
GlobalRole: ptr.String(RoleGitOps),
APIOnly: ptr.Bool(false),
},
license: LicenseInfo{
Tier: TierPremium,
},
checkErr: checkErrCode(ErrAPIOnlyRole),
},
{
name: "global-gitops-modify-api-only-not-set",
create: false,
payload: UserPayload{
GlobalRole: ptr.String(RoleGitOps),
APIOnly: nil,
},
license: LicenseInfo{
Tier: TierPremium,
},
checkErr: nil,
},
{
name: "team-gitops-create-mixed-with-other-roles",
create: true,
payload: UserPayload{
Teams: &[]UserTeam{{Role: RoleGitOps}, {Role: RoleMaintainer}},
APIOnly: ptr.Bool(true),
},
license: LicenseInfo{
Tier: TierPremium,
},
checkErr: nil,
},
{
name: "team-gitops-modify-mixed-with-other-roles",
create: false,
payload: UserPayload{
Teams: &[]UserTeam{{Role: RoleGitOps}, {Role: RoleMaintainer}},
APIOnly: ptr.Bool(true),
},
license: LicenseInfo{
Tier: TierPremium,
},
checkErr: nil,
},
{
name: "team-gitops-create-api-only-false",
create: true,
payload: UserPayload{
Teams: &[]UserTeam{{Role: RoleGitOps}},
APIOnly: ptr.Bool(false),
},
license: LicenseInfo{
Tier: TierPremium,
},
checkErr: checkErrCode(ErrAPIOnlyRole),
},
{
name: "team-gitops-create-api-only-not-set",
create: true,
payload: UserPayload{
Teams: &[]UserTeam{{Role: RoleGitOps}},
APIOnly: nil,
},
license: LicenseInfo{
Tier: TierPremium,
},
checkErr: checkErrCode(ErrAPIOnlyRole),
},
{
name: "team-gitops-modify-to-not-api-only",
create: false,
payload: UserPayload{
Teams: &[]UserTeam{{Role: RoleGitOps}},
APIOnly: ptr.Bool(false),
},
license: LicenseInfo{
Tier: TierPremium,
},
checkErr: checkErrCode(ErrAPIOnlyRole),
},
{
name: "team-gitops-modify-api-only-not-set-should-succeed",
create: false,
payload: UserPayload{
Teams: &[]UserTeam{{Role: RoleGitOps}},
APIOnly: nil, // not updating the APIOnly status.
},
license: LicenseInfo{
Tier: TierPremium,
},
checkErr: nil,
},
{
name: "global-observer-modify-to-not-api-only",
create: false,
payload: UserPayload{
GlobalRole: ptr.String(RoleObserver),
APIOnly: ptr.Bool(false),
},
license: LicenseInfo{
Tier: TierFree,
},
checkErr: nil,
},
{
name: "global-invalid-role",
create: true,
payload: UserPayload{
GlobalRole: ptr.String("foobar"),
},
license: LicenseInfo{
Tier: TierFree,
},
checkErr: checkErrCode(ErrNoRoleNeeded),
},
{
name: "team-invalid-role",
create: true,
payload: UserPayload{
Teams: &[]UserTeam{{Role: "foobar"}},
},
license: LicenseInfo{
Tier: TierFree,
},
checkErr: checkErrCode(ErrNoRoleNeeded),
},
{
name: "global-and-team-role-set",
create: true,
payload: UserPayload{
GlobalRole: ptr.String(RoleObserver),
Teams: &[]UserTeam{{Role: RoleObserver}},
},
license: LicenseInfo{
Tier: TierFree,
},
checkErr: checkErrCode(ErrNoRoleNeeded),
},
{
name: "no-roles-set",
create: true,
payload: UserPayload{
GlobalRole: nil,
Teams: &[]UserTeam{},
},
license: LicenseInfo{
Tier: TierFree,
},
checkErr: checkErrCode(ErrNoRoleNeeded),
},
} {
t.Run(tc.name, func(t *testing.T) {
err := ValidateUserRoles(tc.create, tc.payload, tc.license)
if err == nil {
if tc.checkErr != nil {
t.Errorf("expected an error: %+v", tc)
}
} else { // err != nil
if tc.checkErr == nil {
t.Errorf("unexpected error: %s %+v", err, tc)
} else {
require.True(t, tc.checkErr(err), "err_type=%T, err=%s", err, err)
}
}
})
}
}

View File

@ -17,6 +17,7 @@ import (
"github.com/fleetdm/fleet/v4/server/authz"
authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/contexts/license"
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/go-kit/kit/log/level"
@ -163,17 +164,7 @@ func (svc *Service) AppConfigObfuscated(ctx context.Context) (*fleet.AppConfig,
return nil, err
}
if ac.SMTPSettings.SMTPPassword != "" {
ac.SMTPSettings.SMTPPassword = fleet.MaskedPassword
}
for _, jiraIntegration := range ac.Integrations.Jira {
jiraIntegration.APIToken = fleet.MaskedPassword
}
for _, zdIntegration := range ac.Integrations.Zendesk {
zdIntegration.APIToken = fleet.MaskedPassword
}
ac.Obfuscate()
return ac, nil
}
@ -198,10 +189,9 @@ func modifyAppConfigEndpoint(ctx context.Context, request interface{}, svc fleet
return appConfigResponse{appConfigResponseFields: appConfigResponseFields{Err: err}}, nil
}
license, err := svc.License(ctx)
if err != nil {
return nil, err
}
// We do not use svc.License(ctx) to allow roles (like GitOps) write but not read access to AppConfig.
license, _ := license.FromContext(ctx)
loggingConfig, err := svc.LoggingConfig(ctx)
if err != nil {
return nil, err
@ -238,10 +228,8 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle
}
oldAppConfig := appConfig.Copy()
license, err := svc.License(ctx)
if err != nil {
return nil, err
}
// We do not use svc.License(ctx) to allow roles (like GitOps) write but not read access to AppConfig.
license, _ := license.FromContext(ctx)
oldSmtpSettings := appConfig.SMTPSettings
oldAgentOptions := ""
@ -405,15 +393,16 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle
}
// retrieve new app config with obfuscated secrets
obfuscatedConfig, err := svc.AppConfigObfuscated(ctx)
obfuscatedAppConfig, err := svc.ds.AppConfig(ctx)
if err != nil {
return nil, err
}
obfuscatedAppConfig.Obfuscate()
// if the agent options changed, create the corresponding activity
newAgentOptions := ""
if obfuscatedConfig.AgentOptions != nil {
newAgentOptions = string(*obfuscatedConfig.AgentOptions)
if obfuscatedAppConfig.AgentOptions != nil {
newAgentOptions = string(*obfuscatedAppConfig.AgentOptions)
}
if oldAgentOptions != newAgentOptions {
if err := svc.ds.NewActivity(
@ -460,7 +449,7 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle
}
}
return obfuscatedConfig, nil
return obfuscatedAppConfig, nil
}
func (svc *Service) validateMDM(
@ -645,7 +634,7 @@ func versionEndpoint(ctx context.Context, request interface{}, svc fleet.Service
}
func (svc *Service) Version(ctx context.Context) (*version.Info, error) {
if err := svc.authz.Authorize(ctx, &fleet.AppConfig{}, fleet.ActionRead); err != nil {
if err := svc.authz.Authorize(ctx, &fleet.Version{}, fleet.ActionRead); err != nil {
return nil, err
}

View File

@ -69,6 +69,18 @@ func TestAppConfigAuth(t *testing.T) {
true,
false,
},
{
"global observer+",
&fleet.User{GlobalRole: ptr.String(fleet.RoleObserverPlus)},
true,
false,
},
{
"global gitops",
&fleet.User{GlobalRole: ptr.String(fleet.RoleGitOps)},
false,
true,
},
{
"team admin",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
@ -88,11 +100,23 @@ func TestAppConfigAuth(t *testing.T) {
false,
},
{
"user",
&fleet.User{ID: 777},
"team observer+",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserverPlus}}},
true,
false,
},
{
"team gitops",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleGitOps}}},
true,
true,
},
{
"user without roles",
&fleet.User{ID: 777},
true,
true,
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
@ -104,15 +128,75 @@ func TestAppConfigAuth(t *testing.T) {
_, err = svc.ModifyAppConfig(ctx, []byte(`{}`), fleet.ApplySpecOptions{})
checkAuthErr(t, tt.shouldFailWrite, err)
_, err = svc.Version(ctx)
checkAuthErr(t, tt.shouldFailRead, err)
_, err = svc.CertificateChain(ctx)
checkAuthErr(t, tt.shouldFailRead, err)
})
}
}
// TestVersion tests that all users can access the version endpoint.
func TestVersion(t *testing.T) {
ds := new(mock.Store)
svc, ctx := newTestService(t, ds, nil, nil)
testCases := []struct {
name string
user *fleet.User
}{
{
"global admin",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
},
{
"global maintainer",
&fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
},
{
"global observer",
&fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
},
{
"global observer+",
&fleet.User{GlobalRole: ptr.String(fleet.RoleObserverPlus)},
},
{
"global gitops",
&fleet.User{GlobalRole: ptr.String(fleet.RoleGitOps)},
},
{
"team admin",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
},
{
"team maintainer",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
},
{
"team observer",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
},
{
"team observer+",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserverPlus}}},
},
{
"team gitops",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleGitOps}}},
},
{
"user without roles",
&fleet.User{ID: 777},
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
_, err := svc.Version(ctx)
require.NoError(t, err)
})
}
}
func TestEnrollSecretAuth(t *testing.T) {
ds := new(mock.Store)
svc, ctx := newTestService(t, ds, nil, nil)
@ -387,36 +471,64 @@ func TestAppConfigSecretsObfuscated(t *testing.T) {
}
testCases := []struct {
name string
user *fleet.User
name string
user *fleet.User
shouldFail bool
}{
{
"global admin",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
false,
},
{
"global maintainer",
&fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
false,
},
{
"global observer",
&fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
false,
},
{
"global observer+",
&fleet.User{GlobalRole: ptr.String(fleet.RoleObserverPlus)},
false,
},
{
"global gitops",
&fleet.User{GlobalRole: ptr.String(fleet.RoleGitOps)},
true,
},
{
"team admin",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
false,
},
{
"team maintainer",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
false,
},
{
"team observer",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
false,
},
{
"user",
"team observer+",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserverPlus}}},
false,
},
{
"team gitops",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleGitOps}}},
true,
},
{
"user without roles",
&fleet.User{ID: 777},
true,
},
}
for _, tt := range testCases {
@ -424,10 +536,14 @@ func TestAppConfigSecretsObfuscated(t *testing.T) {
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
ac, err := svc.AppConfigObfuscated(ctx)
require.NoError(t, err)
require.Equal(t, ac.SMTPSettings.SMTPPassword, fleet.MaskedPassword)
require.Equal(t, ac.Integrations.Jira[0].APIToken, fleet.MaskedPassword)
require.Equal(t, ac.Integrations.Zendesk[0].APIToken, fleet.MaskedPassword)
if tt.shouldFail {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, ac.SMTPSettings.SMTPPassword, fleet.MaskedPassword)
require.Equal(t, ac.Integrations.Jira[0].APIToken, fleet.MaskedPassword)
require.Equal(t, ac.Integrations.Zendesk[0].APIToken, fleet.MaskedPassword)
}
})
}
}

View File

@ -174,10 +174,6 @@ func deleteGlobalPoliciesEndpoint(ctx context.Context, request interface{}, svc
// DeleteGlobalPolicies deletes the given policies from the database.
// It also deletes the given ids from the failing policies webhook configuration.
func (svc Service) DeleteGlobalPolicies(ctx context.Context, ids []uint) ([]uint, error) {
// First check if authorized to read policies
if err := svc.authz.Authorize(ctx, &fleet.Policy{}, fleet.ActionRead); err != nil {
return nil, err
}
if len(ids) == 0 {
return nil, nil
}
@ -185,7 +181,6 @@ func (svc Service) DeleteGlobalPolicies(ctx context.Context, ids []uint) ([]uint
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting policies by ID")
}
// Then check if authorized to write policies
if err := svc.authz.Authorize(ctx, &fleet.Policy{}, fleet.ActionWrite); err != nil {
return nil, err
}

View File

@ -525,11 +525,6 @@ func TestListHosts(t *testing.T) {
require.NoError(t, err)
require.Len(t, hosts, 1)
// anyone can list hosts
hosts, err = svc.ListHosts(test.UserContext(ctx, test.UserNoRoles), fleet.HostListOptions{})
require.NoError(t, err)
require.Len(t, hosts, 1)
// a user is required
_, err = svc.ListHosts(ctx, fleet.HostListOptions{})
require.Error(t, err)
@ -568,9 +563,6 @@ func TestGetHostSummary(t *testing.T) {
require.Len(t, summary.BuiltinLabels, 1)
require.Equal(t, "All hosts", summary.BuiltinLabels[0].Name)
_, err = svc.GetHostSummary(test.UserContext(ctx, test.UserNoRoles), nil, nil, nil)
require.NoError(t, err)
// a user is required
_, err = svc.GetHostSummary(ctx, nil, nil, nil)
require.Error(t, err)

View File

@ -61,6 +61,7 @@ func (s *integrationEnterpriseTestSuite) SetupSuite() {
s.server = server
s.users = users
s.token = s.getTestAdminToken()
s.cachedTokens = make(map[string]string)
}
func (s *integrationEnterpriseTestSuite) TearDownTest() {
@ -271,6 +272,77 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
assert.Equal(t, "ABC", team.Secrets[0].Secret)
}
func (s *integrationEnterpriseTestSuite) TestTeamSpecsPermissions() {
t := s.T()
//
// Setup test
//
// Create two teams, team1 and team2.
team1, err := s.ds.NewTeam(context.Background(), &fleet.Team{
ID: 42,
Name: "team1",
Description: "desc team1",
})
require.NoError(t, err)
team2, err := s.ds.NewTeam(context.Background(), &fleet.Team{
ID: 43,
Name: "team2",
Description: "desc team2",
})
require.NoError(t, err)
// Create a new admin for team1.
password := test.GoodPassword
email := "admin-team1@example.com"
u := &fleet.User{
Name: "admin team1",
Email: email,
GlobalRole: nil,
Teams: []fleet.UserTeam{
{
Team: *team1,
Role: fleet.RoleAdmin,
},
},
}
require.NoError(t, u.SetPassword(password, 10, 10))
_, err = s.ds.NewUser(context.Background(), u)
require.NoError(t, err)
//
// Start testing team specs with admin of team1.
//
s.setTokenForTest(t, "admin-team1@example.com", test.GoodPassword)
// Should allow editing own team.
agentOpts := json.RawMessage(`{"config": {"views": {"foo": "bar2"}}, "overrides": {"platforms": {"darwin": {"views": {"bar": "qux"}}}}}`)
editTeam1Spec := applyTeamSpecsRequest{
Specs: []*fleet.TeamSpec{
{
Name: team1.Name,
AgentOptions: agentOpts,
},
},
}
s.Do("POST", "/api/latest/fleet/spec/teams", editTeam1Spec, http.StatusOK)
team1b, err := s.ds.Team(context.Background(), team1.ID)
require.NoError(t, err)
require.Equal(t, *team1b.Config.AgentOptions, agentOpts)
// Should not allow editing other teams.
editTeam2Spec := applyTeamSpecsRequest{
Specs: []*fleet.TeamSpec{
{
Name: team2.Name,
AgentOptions: agentOpts,
},
},
}
s.Do("POST", "/api/latest/fleet/spec/teams", editTeam2Spec, http.StatusForbidden)
}
func (s *integrationEnterpriseTestSuite) TestTeamSchedule() {
t := s.T()
@ -2512,3 +2584,659 @@ func (s *integrationEnterpriseTestSuite) TestListSoftware() {
require.NotNil(t, barPayload.Vulnerabilities[0].CISAKnownExploit, ptr.BoolPtr(true))
require.Equal(t, barPayload.Vulnerabilities[0].CVEPublished, ptr.TimePtr(now))
}
// TestGitOpsUserActions tests the permissions listed in ../../docs/Using-Fleet/Permissions.md.
func (s *integrationEnterpriseTestSuite) TestGitOpsUserActions() {
t := s.T()
ctx := context.Background()
//
// Setup test data.
// All actions are authored by a global admin.
//
admin, err := s.ds.UserByEmail(ctx, "admin1@example.com")
require.NoError(t, err)
h1, err := s.ds.NewHost(ctx, &fleet.Host{
NodeKey: ptr.String(t.Name() + "1"),
UUID: t.Name() + "1",
Hostname: t.Name() + "foo.local",
})
require.NoError(t, err)
t1, err := s.ds.NewTeam(ctx, &fleet.Team{
Name: "Foo",
})
require.NoError(t, err)
t2, err := s.ds.NewTeam(ctx, &fleet.Team{
Name: "Bar",
})
require.NoError(t, err)
t3, err := s.ds.NewTeam(ctx, &fleet.Team{
Name: "Zoo",
})
require.NoError(t, err)
acr := appConfigResponse{}
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
"webhook_settings": {
"vulnerabilities_webhook": {
"enable_vulnerabilities_webhook": false
}
}
}`), http.StatusOK, &acr)
s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acr)
require.False(t, acr.WebhookSettings.VulnerabilitiesWebhook.Enable)
q1, err := s.ds.NewQuery(ctx, &fleet.Query{
Name: "Foo",
Query: "SELECT * from time;",
})
require.NoError(t, err)
ggsr := getGlobalScheduleResponse{}
s.DoJSON("GET", "/api/latest/fleet/schedule", nil, http.StatusOK, &ggsr)
require.NoError(t, ggsr.Err)
var globalPackID uint
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(context.Background(), q, &globalPackID,
`SELECT id FROM packs WHERE pack_type = 'global'`)
})
require.NotZero(t, globalPackID)
cur := createUserResponse{}
s.DoJSON("POST", "/api/latest/fleet/users/admin", createUserRequest{
UserPayload: fleet.UserPayload{
Email: ptr.String("foo42@example.com"),
Password: ptr.String("p4ssw0rd.123"),
Name: ptr.String("foo42"),
GlobalRole: ptr.String("maintainer"),
},
}, http.StatusOK, &cur)
maintainer := cur.User
var carveBeginResp carveBeginResponse
s.DoJSON("POST", "/api/osquery/carve/begin", carveBeginRequest{
NodeKey: *h1.NodeKey,
BlockCount: 3,
BlockSize: 3,
CarveSize: 8,
CarveId: "c1",
RequestId: "r1",
}, http.StatusOK, &carveBeginResp)
require.NotEmpty(t, carveBeginResp.SessionId)
lcr := listCarvesResponse{}
s.DoJSON("GET", "/api/latest/fleet/carves", listCarvesRequest{}, http.StatusOK, &lcr)
require.NotEmpty(t, lcr.Carves)
carveID := lcr.Carves[0].ID
// Create the global GitOps user we'll use in tests.
u := &fleet.User{
Name: "GitOps",
Email: "gitops1@example.com",
GlobalRole: ptr.String(fleet.RoleGitOps),
}
require.NoError(t, u.SetPassword(test.GoodPassword, 10, 10))
_, err = s.ds.NewUser(context.Background(), u)
require.NoError(t, err)
// Create a GitOps user for team t1 we'll use in tests.
u2 := &fleet.User{
Name: "GitOps 2",
Email: "gitops2@example.com",
GlobalRole: nil,
Teams: []fleet.UserTeam{
{
Team: *t1,
Role: fleet.RoleGitOps,
},
{
Team: *t3,
Role: fleet.RoleGitOps,
},
},
}
require.NoError(t, u2.SetPassword(test.GoodPassword, 10, 10))
_, err = s.ds.NewUser(context.Background(), u2)
require.NoError(t, err)
gp2, err := s.ds.NewGlobalPolicy(ctx, &admin.ID, fleet.PolicyPayload{
Name: "Zoo",
Query: "SELECT 0;",
})
require.NoError(t, err)
t2p, err := s.ds.NewTeamPolicy(ctx, t2.ID, &admin.ID, fleet.PolicyPayload{
Name: "Zoo2",
Query: "SELECT 2;",
})
require.NoError(t, err)
// Create some test user to test moving from/to teams.
u3 := &fleet.User{
Name: "Test Foo Observer",
Email: "test-foo-observer@example.com",
GlobalRole: nil,
Teams: []fleet.UserTeam{
{
Team: *t1,
Role: fleet.RoleObserver,
},
},
}
require.NoError(t, u3.SetPassword(test.GoodPassword, 10, 10))
_, err = s.ds.NewUser(context.Background(), u3)
require.NoError(t, err)
//
// Start running permission tests with user gitops1.
//
s.setTokenForTest(t, "gitops1@example.com", test.GoodPassword)
// Attempt to retrieve activities, should fail.
s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusForbidden, &listActivitiesResponse{})
// Attempt to retrieve hosts, should fail.
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusForbidden, &listHostsResponse{})
// Attempt to filter hosts using labels, should fail (label ID 6 is the builtin label "All Hosts")
s.DoJSON("GET", "/api/latest/fleet/labels/6/hosts", nil, http.StatusForbidden, &listHostsResponse{})
// Attempt to delete hosts, should fail.
s.DoJSON("DELETE", "/api/latest/fleet/hosts/1", nil, http.StatusForbidden, &deleteHostResponse{})
// Attempt to transfer host from global to a team, should allow.
s.DoJSON("POST", "/api/latest/fleet/hosts/transfer", addHostsToTeamRequest{
TeamID: &t1.ID,
HostIDs: []uint{h1.ID},
}, http.StatusOK, &addHostsToTeamResponse{})
// Attempt to create a label, should allow.
clr := createLabelResponse{}
s.DoJSON("POST", "/api/latest/fleet/labels", createLabelRequest{
LabelPayload: fleet.LabelPayload{
Name: ptr.String("foo"),
Query: ptr.String("SELECT 1;"),
},
}, http.StatusOK, &clr)
// Attempt to modify a label, should allow.
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/labels/%d", clr.Label.ID), modifyLabelRequest{
ModifyLabelPayload: fleet.ModifyLabelPayload{
Name: ptr.String("foo2"),
},
}, http.StatusOK, &modifyLabelResponse{})
// Attempt to get a label, should fail.
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/labels/%d", clr.Label.ID), getLabelRequest{}, http.StatusForbidden, &getLabelResponse{})
// Attempt to list all labels, should fail.
s.DoJSON("GET", "/api/latest/fleet/labels", listLabelsRequest{}, http.StatusForbidden, &listLabelsResponse{})
// Attempt to delete a label, should allow.
s.DoJSON("DELETE", "/api/latest/fleet/labels/foo2", deleteLabelRequest{}, http.StatusOK, &deleteLabelResponse{})
// Attempt to list all software, should fail.
s.DoJSON("GET", "/api/latest/fleet/software", listSoftwareRequest{}, http.StatusForbidden, &listSoftwareResponse{})
s.DoJSON("GET", "/api/latest/fleet/software/count", countSoftwareRequest{}, http.StatusForbidden, &countSoftwareResponse{})
// Attempt to list a software, should fail.
s.DoJSON("GET", "/api/latest/fleet/software/1", getSoftwareRequest{}, http.StatusForbidden, &getSoftwareResponse{})
// Attempt to read app config, should fail.
s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusForbidden, &appConfigResponse{})
// Attempt to write app config, should allow.
acr = appConfigResponse{}
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
"webhook_settings": {
"vulnerabilities_webhook": {
"enable_vulnerabilities_webhook": true,
"destination_url": "https://foobar.example.com"
}
}
}`), http.StatusOK, &acr)
require.True(t, acr.AppConfig.WebhookSettings.VulnerabilitiesWebhook.Enable)
require.Equal(t, "https://foobar.example.com", acr.AppConfig.WebhookSettings.VulnerabilitiesWebhook.DestinationURL)
// Attempt to run live queries synchronously, should fail.
// TODO(lucas): This is a bug, the synchronous live query API should return 403 but currently returns 200.
// It doesn't run the query but incorrectly returns a 200.
s.DoJSON("GET", "/api/latest/fleet/queries/run", runLiveQueryRequest{
HostIDs: []uint{h1.ID},
QueryIDs: []uint{q1.ID},
}, http.StatusOK, &runLiveQueryResponse{})
// Attempt to run live queries asynchronously (new unsaved query), should fail.
s.DoJSON("POST", "/api/latest/fleet/queries/run", createDistributedQueryCampaignRequest{
QuerySQL: "SELECT * FROM time;",
Selected: fleet.HostTargets{
HostIDs: []uint{h1.ID},
},
}, http.StatusForbidden, &runLiveQueryResponse{})
// Attempt to run live queries asynchronously (saved query), should fail.
s.DoJSON("POST", "/api/latest/fleet/queries/run", createDistributedQueryCampaignRequest{
QueryID: ptr.Uint(q1.ID),
Selected: fleet.HostTargets{
HostIDs: []uint{h1.ID},
},
}, http.StatusForbidden, &runLiveQueryResponse{})
// Attempt to create queries, should allow.
cqr := createQueryResponse{}
s.DoJSON("POST", "/api/latest/fleet/queries", createQueryRequest{
QueryPayload: fleet.QueryPayload{
Name: ptr.String("foo4"),
Query: ptr.String("SELECT * from osquery_info;"),
},
}, http.StatusOK, &cqr)
cqr2 := createQueryResponse{}
s.DoJSON("POST", "/api/latest/fleet/queries", createQueryRequest{
QueryPayload: fleet.QueryPayload{
Name: ptr.String("foo5"),
Query: ptr.String("SELECT * from os_version;"),
},
}, http.StatusOK, &cqr2)
cqr3 := createQueryResponse{}
s.DoJSON("POST", "/api/latest/fleet/queries", createQueryRequest{
QueryPayload: fleet.QueryPayload{
Name: ptr.String("foo6"),
Query: ptr.String("SELECT * from processes;"),
},
}, http.StatusOK, &cqr3)
cqr4 := createQueryResponse{}
s.DoJSON("POST", "/api/latest/fleet/queries", createQueryRequest{
QueryPayload: fleet.QueryPayload{
Name: ptr.String("foo7"),
Query: ptr.String("SELECT * from managed_policies;"),
},
}, http.StatusOK, &cqr4)
// Attempt to edit queries, should allow.
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", cqr.Query.ID), modifyQueryRequest{
QueryPayload: fleet.QueryPayload{
Name: ptr.String("foo4"),
Query: ptr.String("SELECT * FROM system_info;"),
},
}, http.StatusOK, &modifyQueryResponse{})
// Attempt to view a query, should fail.
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d", cqr.Query.ID), getQueryRequest{}, http.StatusForbidden, &getQueryResponse{})
// Attempt to list all queries, should fail.
s.DoJSON("GET", "/api/latest/fleet/queries", listQueriesRequest{}, http.StatusForbidden, &listQueriesResponse{})
// Attempt to delete queries, should allow.
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/queries/id/%d", cqr.Query.ID), deleteQueryByIDRequest{}, http.StatusOK, &deleteQueryByIDResponse{})
s.DoJSON("POST", "/api/latest/fleet/queries/delete", deleteQueriesRequest{IDs: []uint{cqr2.Query.ID}}, http.StatusOK, &deleteQueriesResponse{})
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/queries/%s", cqr3.Query.Name), deleteQueryRequest{}, http.StatusOK, &deleteQueryResponse{})
// Attempt to add a query to the global schedule, should allow.
sqr := scheduleQueryResponse{}
s.DoJSON("POST", "/api/latest/fleet/packs/schedule", scheduleQueryRequest{
PackID: globalPackID,
QueryID: cqr4.Query.ID,
Interval: 60,
}, http.StatusOK, &sqr)
// Attempt to edit a scheduled query in the global schedule, should allow.
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/packs/schedule/%d", sqr.Scheduled.ID), modifyScheduledQueryRequest{
ScheduledQueryPayload: fleet.ScheduledQueryPayload{
Interval: ptr.Uint(30),
},
}, http.StatusOK, &scheduleQueryResponse{})
// Attempt to remove a query from the global schedule, should allow.
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/packs/schedule/%d", sqr.Scheduled.ID), deleteScheduledQueryRequest{}, http.StatusOK, &scheduleQueryResponse{})
// Attempt to read the global schedule, should allow.
// This is an exception to the "write only" nature of gitops (packs can be viewed by gitops).
s.DoJSON("GET", "/api/latest/fleet/schedule", nil, http.StatusOK, &getGlobalScheduleResponse{})
// Attempt to create a pack, should allow.
cpr := createPackResponse{}
s.DoJSON("POST", "/api/latest/fleet/packs", createPackRequest{
PackPayload: fleet.PackPayload{
Name: ptr.String("foo8"),
},
}, http.StatusOK, &cpr)
// Attempt to edit a pack, should allow.
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/packs/%d", cpr.Pack.ID), modifyPackRequest{
PackPayload: fleet.PackPayload{
Name: ptr.String("foo9"),
},
}, http.StatusOK, &modifyPackResponse{})
// Attempt to read a pack, should allow.
// This is an exception to the "write only" nature of gitops (packs can be viewed by gitops).
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/packs/%d", cpr.Pack.ID), nil, http.StatusOK, &getPackResponse{})
// Attempt to delete a pack, should allow.
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/packs/id/%d", cpr.Pack.ID), deletePackRequest{}, http.StatusOK, &deletePackResponse{})
// Attempt to create a global policy, should allow.
gplr := globalPolicyResponse{}
s.DoJSON("POST", "/api/latest/fleet/policies", globalPolicyRequest{
Name: "foo9",
Query: "SELECT * from plist;",
}, http.StatusOK, &gplr)
// Attempt to edit a global policy, should allow.
mgplr := modifyGlobalPolicyResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/policies/%d", gplr.Policy.ID), modifyGlobalPolicyRequest{
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
Query: ptr.String("SELECT * from plist WHERE path = 'foo';"),
},
}, http.StatusOK, &mgplr)
// Attempt to read a global policy, should fail.
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/policies/%d", gplr.Policy.ID), getPolicyByIDRequest{}, http.StatusForbidden, &getPolicyByIDResponse{})
// Attempt to delete a global policy, should allow.
s.DoJSON("POST", "/api/latest/fleet/policies/delete", deleteGlobalPoliciesRequest{
IDs: []uint{gplr.Policy.ID},
}, http.StatusOK, &deleteGlobalPoliciesResponse{})
// Attempt to create a team policy, should allow.
tplr := teamPolicyResponse{}
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/team/%d/policies", t1.ID), teamPolicyRequest{
Name: "foo10",
Query: "SELECT * from file;",
}, http.StatusOK, &tplr)
// Attempt to edit a team policy, should allow.
mtplr := modifyTeamPolicyResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", t1.ID, tplr.Policy.ID), modifyTeamPolicyRequest{
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
Query: ptr.String("SELECT * from file WHERE path = 'foo';"),
},
}, http.StatusOK, &mtplr)
// Attempt to view a team policy, should fail.
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/team/%d/policies/%d", t1.ID, tplr.Policy.ID), getTeamPolicyByIDRequest{}, http.StatusForbidden, &getTeamPolicyByIDResponse{})
// Attempt to delete a team policy, should allow.
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/delete", t1.ID), deleteTeamPoliciesRequest{
IDs: []uint{tplr.Policy.ID},
}, http.StatusOK, &deleteTeamPoliciesResponse{})
// Attempt to create a user, should fail.
s.DoJSON("POST", "/api/latest/fleet/users/admin", createUserRequest{
UserPayload: fleet.UserPayload{
Email: ptr.String("foo10@example.com"),
Name: ptr.String("foo10"),
GlobalRole: ptr.String("admin"),
},
}, http.StatusForbidden, &createUserResponse{})
// Attempt to modify a user, should fail.
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/users/%d", admin.ID), modifyUserRequest{
UserPayload: fleet.UserPayload{
GlobalRole: ptr.String("observer"),
},
}, http.StatusForbidden, &modifyUserResponse{})
// Attempt to view a user, should fail.
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/users/%d", admin.ID), getUserRequest{}, http.StatusForbidden, &getUserResponse{})
// Attempt to delete a user, should fail.
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/users/%d", admin.ID), deleteUserRequest{}, http.StatusForbidden, &deleteUserResponse{})
// Attempt to add users to team, should allow.
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/users", t1.ID), modifyTeamUsersRequest{
Users: []fleet.TeamUser{
{
User: *maintainer,
Role: "admin",
},
},
}, http.StatusOK, &teamResponse{})
// Attempt to delete users from team, should allow.
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/teams/%d/users", t1.ID), modifyTeamUsersRequest{
Users: []fleet.TeamUser{
{
User: *maintainer,
Role: "admin",
},
},
}, http.StatusOK, &teamResponse{})
// Attempt to create a team, should allow.
tr := teamResponse{}
s.DoJSON("POST", "/api/latest/fleet/teams", createTeamRequest{
TeamPayload: fleet.TeamPayload{
Name: ptr.String("foo11"),
},
}, http.StatusOK, &tr)
// Attempt to edit a team, should allow.
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", tr.Team.ID), modifyTeamRequest{
TeamPayload: fleet.TeamPayload{
Name: ptr.String("foo12"),
},
}, http.StatusOK, &teamResponse{})
// Attempt to edit a team's agent options, should allow.
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/agent_options", tr.Team.ID), json.RawMessage(`{
"config": {
"options": {
"aws_debug": true
}
}
}`), http.StatusOK, &teamResponse{})
// Attempt to view a team, should fail.
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", tr.Team.ID), getTeamRequest{}, http.StatusForbidden, &teamResponse{})
// Attempt to delete a team, should allow.
dtr := deleteTeamResponse{}
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/teams/%d", tr.Team.ID), deleteTeamRequest{}, http.StatusOK, &dtr)
// Attempt to create/edit enroll secrets, should allow.
s.DoJSON("POST", "/api/latest/fleet/spec/enroll_secret", applyEnrollSecretSpecRequest{
Spec: &fleet.EnrollSecretSpec{
Secrets: []*fleet.EnrollSecret{
{
Secret: "foo400",
TeamID: nil,
},
{
Secret: "foo500",
TeamID: ptr.Uint(t1.ID),
},
},
},
}, http.StatusOK, &applyEnrollSecretSpecResponse{})
// Attempt to get enroll secrets, should fail.
s.DoJSON("GET", "/api/latest/fleet/spec/enroll_secret", nil, http.StatusForbidden, &getEnrollSecretSpecResponse{})
// Attempt to get team enroll secret, should fail.
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d/secrets", t1.ID), teamEnrollSecretsRequest{}, http.StatusForbidden, &teamEnrollSecretsResponse{})
// Attempt to list carved files, should fail.
s.DoJSON("GET", "/api/latest/fleet/carves", listCarvesRequest{}, http.StatusForbidden, &listCarvesResponse{})
// Attempt to get a carved file, should fail.
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/carves/%d", carveID), listCarvesRequest{}, http.StatusForbidden, &listCarvesResponse{})
//
// Start running permission tests with user gitops2 (which is a GitOps use for team t1).
//
s.setTokenForTest(t, "gitops2@example.com", test.GoodPassword)
// Attempt to create queries, should allow.
tcqr := createQueryResponse{}
s.DoJSON("POST", "/api/latest/fleet/queries", createQueryRequest{
QueryPayload: fleet.QueryPayload{
Name: ptr.String("foo600"),
Query: ptr.String("SELECT * from orbit_info;"),
},
}, http.StatusOK, &tcqr)
// Attempt to edit own query, should allow.
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", tcqr.Query.ID), modifyQueryRequest{
QueryPayload: fleet.QueryPayload{
Name: ptr.String("foo4"),
Query: ptr.String("SELECT * FROM system_info;"),
},
}, http.StatusOK, &modifyQueryResponse{})
// Attempt to delete own query, should allow.
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/queries/id/%d", tcqr.Query.ID), deleteQueryByIDRequest{}, http.StatusOK, &deleteQueryByIDResponse{})
// Attempt to edit query created by somebody else, should fail.
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", cqr4.Query.ID), modifyQueryRequest{
QueryPayload: fleet.QueryPayload{
Name: ptr.String("foo4"),
Query: ptr.String("SELECT * FROM system_info;"),
},
}, http.StatusForbidden, &modifyQueryResponse{})
// Attempt to delete query created by somebody else, should fail.
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/queries/id/%d", cqr4.Query.ID), deleteQueryByIDRequest{}, http.StatusForbidden, &deleteQueryByIDResponse{})
// Attempt to read the global schedule, should fail.
s.DoJSON("GET", "/api/latest/fleet/schedule", nil, http.StatusForbidden, &getGlobalScheduleResponse{})
// Attempt to read the team's schedule, should fail.
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d/schedule", t1.ID), getTeamScheduleRequest{}, http.StatusForbidden, &getTeamScheduleResponse{})
// Attempt to read other team's schedule, should fail.
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d/schedule", t2.ID), getTeamScheduleRequest{}, http.StatusForbidden, &getTeamScheduleResponse{})
// Attempt to add a query to the global schedule, should fail.
tsqr := scheduleQueryResponse{}
s.DoJSON("POST", "/api/latest/fleet/packs/schedule", scheduleQueryRequest{
PackID: globalPackID,
QueryID: cqr4.Query.ID,
Interval: 60,
}, http.StatusForbidden, &tsqr)
// Attempt to add a query to the team's schedule, should allow.
ttsqr := teamScheduleQueryResponse{}
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/schedule", t1.ID), teamScheduleQueryRequest{
ScheduledQueryPayload: fleet.ScheduledQueryPayload{
QueryID: ptr.Uint(cqr4.Query.ID),
Interval: ptr.Uint(60),
},
}, http.StatusOK, &ttsqr)
// Attempt to remove a query from the team's schedule, should allow.
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/teams/%d/schedule/%d", t1.ID, ttsqr.Scheduled.ID), deleteTeamScheduleRequest{}, http.StatusOK, &deleteTeamScheduleResponse{})
// Attempt to read the global schedule, should fail.
s.DoJSON("GET", "/api/latest/fleet/schedule", nil, http.StatusForbidden, &getGlobalScheduleResponse{})
// Attempt to read a global policy, should fail.
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/policies/%d", gp2.ID), getPolicyByIDRequest{}, http.StatusForbidden, &getPolicyByIDResponse{})
// Attempt to delete a global policy, should fail.
s.DoJSON("POST", "/api/latest/fleet/policies/delete", deleteGlobalPoliciesRequest{
IDs: []uint{gp2.ID},
}, http.StatusForbidden, &deleteGlobalPoliciesResponse{})
// Attempt to create a team policy, should allow.
ttplr := teamPolicyResponse{}
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/team/%d/policies", t1.ID), teamPolicyRequest{
Name: "foo1000",
Query: "SELECT * from file;",
}, http.StatusOK, &ttplr)
// Attempt to edit a team policy, should allow.
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", t1.ID, ttplr.Policy.ID), modifyTeamPolicyRequest{
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
Query: ptr.String("SELECT * from file WHERE path = 'foobar';"),
},
}, http.StatusOK, &modifyTeamPolicyResponse{})
// Attempt to edit another team's policy, should fail.
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", t2.ID, t2p.ID), modifyTeamPolicyRequest{
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
Query: ptr.String("SELECT * from file WHERE path = 'foobar';"),
},
}, http.StatusForbidden, &modifyTeamPolicyResponse{})
// Attempt to view a team policy, should fail.
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/team/%d/policies/%d", t1.ID, ttplr.Policy.ID), getTeamPolicyByIDRequest{}, http.StatusForbidden, &getTeamPolicyByIDResponse{})
// Attempt to view another team's policy, should fail.
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/team/%d/policies/%d", t2.ID, t2p.ID), getTeamPolicyByIDRequest{}, http.StatusForbidden, &getTeamPolicyByIDResponse{})
// Attempt to delete a team policy, should allow.
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/delete", t1.ID), deleteTeamPoliciesRequest{
IDs: []uint{ttplr.Policy.ID},
}, http.StatusOK, &deleteTeamPoliciesResponse{})
// Attempt to edit own team, should allow.
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", t1.ID), modifyTeamRequest{
TeamPayload: fleet.TeamPayload{
Name: ptr.String("foo123456"),
},
}, http.StatusOK, &teamResponse{})
// Attempt to edit another team, should fail.
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", t2.ID), modifyTeamRequest{
TeamPayload: fleet.TeamPayload{
Name: ptr.String("foo123456"),
},
}, http.StatusForbidden, &teamResponse{})
// Attempt to edit own team's agent options, should allow.
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/agent_options", t1.ID), json.RawMessage(`{
"config": {
"options": {
"aws_debug": true
}
}
}`), http.StatusOK, &teamResponse{})
// Attempt to edit another team's agent options, should fail.
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/agent_options", t2.ID), json.RawMessage(`{
"config": {
"options": {
"aws_debug": true
}
}
}`), http.StatusForbidden, &teamResponse{})
// Attempt to add users from team it owns to another team it owns, should allow.
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/users", t3.ID), modifyTeamUsersRequest{
Users: []fleet.TeamUser{
{
User: *u3,
Role: "maintainer",
},
},
}, http.StatusOK, &teamResponse{})
// Attempt to delete users from team it owns, should allow.
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/teams/%d/users", t3.ID), modifyTeamUsersRequest{
Users: []fleet.TeamUser{
{
User: *u3,
},
},
}, http.StatusOK, &teamResponse{})
// Attempt to add users to another team it doesn't own, should fail.
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/users", t2.ID), modifyTeamUsersRequest{
Users: []fleet.TeamUser{
{
User: *u3,
Role: "maintainer",
},
},
}, http.StatusForbidden, &teamResponse{})
// Attempt to delete users from team it doesn't own, should fail.
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/teams/%d/users", t2.ID), modifyTeamUsersRequest{
Users: []fleet.TeamUser{
{
User: *u2,
},
},
}, http.StatusForbidden, &teamResponse{})
}
func (s *integrationEnterpriseTestSuite) setTokenForTest(t *testing.T, email, password string) {
oldToken := s.token
t.Cleanup(func() {
s.token = oldToken
})
s.token = s.getCachedUserToken(email, password)
}

View File

@ -198,9 +198,9 @@ func (s *integrationMDMTestSuite) TearDownTest() {
t := s.T()
ctx := context.Background()
s.token = s.getTestAdminToken()
appCfg := s.getConfig()
if appCfg.MDM.MacOSSettings.EnableDiskEncryption {
s.token = s.getTestAdminToken()
// ensure global disk encryption is disabled on exit
s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
"mdm": { "macos_settings": { "enable_disk_encryption": false } }
@ -2739,7 +2739,8 @@ func (s *integrationMDMTestSuite) assertConfigProfilesByIdentifier(teamID *uint,
// generates the body and headers part of a multipart request ready to be
// used via s.DoRawWithHeaders to POST /api/_version_/fleet/mdm/apple/profiles.
func generateNewProfileMultipartRequest(t *testing.T, tmID *uint,
fileName string, fileContent []byte, token string) (*bytes.Buffer, map[string]string) {
fileName string, fileContent []byte, token string,
) (*bytes.Buffer, map[string]string) {
var body bytes.Buffer
writer := multipart.NewWriter(&body)

View File

@ -222,8 +222,13 @@ func modifyQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Ser
}
func (svc *Service) ModifyQuery(ctx context.Context, id uint, p fleet.QueryPayload) (*fleet.Query, error) {
// First make sure the user can read queries
if err := svc.authz.Authorize(ctx, &fleet.Query{}, fleet.ActionRead); err != nil {
query, err := svc.ds.Query(ctx, id)
if err != nil {
setAuthCheckedOnPreAuthErr(ctx)
return nil, err
}
if err := svc.authz.Authorize(ctx, query, fleet.ActionWrite); err != nil {
return nil, err
}
@ -233,16 +238,6 @@ func (svc *Service) ModifyQuery(ctx context.Context, id uint, p fleet.QueryPaylo
})
}
query, err := svc.ds.Query(ctx, id)
if err != nil {
return nil, err
}
// Then we make sure they can modify them
if err := svc.authz.Authorize(ctx, query, fleet.ActionWrite); err != nil {
return nil, err
}
if p.Name != nil {
query.Name = *p.Name
}
@ -303,17 +298,12 @@ func deleteQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Ser
}
func (svc *Service) DeleteQuery(ctx context.Context, name string) error {
// First make sure the user can read queries
if err := svc.authz.Authorize(ctx, &fleet.Query{}, fleet.ActionRead); err != nil {
return err
}
query, err := svc.ds.QueryByName(ctx, name)
if err != nil {
setAuthCheckedOnPreAuthErr(ctx)
return err
}
// Then we make sure they can modify them
if err := svc.authz.Authorize(ctx, query, fleet.ActionWrite); err != nil {
return err
}
@ -358,17 +348,12 @@ func deleteQueryByIDEndpoint(ctx context.Context, request interface{}, svc fleet
}
func (svc *Service) DeleteQueryByID(ctx context.Context, id uint) error {
// First make sure the user can read queries
if err := svc.authz.Authorize(ctx, &fleet.Query{}, fleet.ActionRead); err != nil {
return err
}
query, err := svc.ds.Query(ctx, id)
if err != nil {
setAuthCheckedOnPreAuthErr(ctx)
return ctxerr.Wrap(ctx, err, "lookup query by ID")
}
// Then we make sure they can modify them
if err := svc.authz.Authorize(ctx, query, fleet.ActionWrite); err != nil {
return err
}
@ -414,18 +399,13 @@ func deleteQueriesEndpoint(ctx context.Context, request interface{}, svc fleet.S
}
func (svc *Service) DeleteQueries(ctx context.Context, ids []uint) (uint, error) {
// First make sure the user can read queries
if err := svc.authz.Authorize(ctx, &fleet.Query{}, fleet.ActionRead); err != nil {
return 0, err
}
for _, id := range ids {
query, err := svc.ds.Query(ctx, id)
if err != nil {
setAuthCheckedOnPreAuthErr(ctx)
return 0, ctxerr.Wrap(ctx, err, "lookup query by ID")
}
// Then we make sure they can modify them
if err := svc.authz.Authorize(ctx, query, fleet.ActionWrite); err != nil {
return 0, err
}
@ -472,7 +452,6 @@ func applyQuerySpecsEndpoint(ctx context.Context, request interface{}, svc fleet
}
func (svc *Service) ApplyQuerySpecs(ctx context.Context, specs []*fleet.QuerySpec) error {
// check that the user can create queries
if err := svc.authz.Authorize(ctx, &fleet.Query{}, fleet.ActionWrite); err != nil {
return err
}

View File

@ -133,10 +133,6 @@ func (svc *Service) VulnerabilitiesConfig(ctx context.Context) (*fleet.Vulnerabi
}
func (svc *Service) LoggingConfig(ctx context.Context) (*fleet.Logging, error) {
if err := svc.authz.Authorize(ctx, &fleet.AppConfig{}, fleet.ActionRead); err != nil {
return nil, err
}
conf := svc.config
logging := &fleet.Logging{
Debug: conf.Logging.Debug,

View File

@ -414,7 +414,8 @@ func TestService_EmailConfig(t *testing.T) {
Config: fleet.SESConfig{
Region: "us-east-1",
SourceARN: "qux",
}},
},
},
wantErr: assert.NoError,
},
{
@ -429,15 +430,17 @@ func TestService_EmailConfig(t *testing.T) {
wantErr: assert.NoError,
},
{
name: "no configured email backend should return nil",
name: "accessing without roles should return forbidden",
fields: fields{
config: config.TestConfig(),
config: testSESPluginConfig(),
},
args: args{
ctx: test.UserContext(context.Background(), test.UserNoRoles),
},
want: nil,
wantErr: assert.NoError,
want: nil,
wantErr: func(tt assert.TestingT, err error, i ...interface{}) bool {
return assert.EqualError(tt, err, "forbidden")
},
},
}
for _, tt := range tests {

View File

@ -35,7 +35,7 @@ func (svc *Service) NewUser(ctx context.Context, p fleet.UserPayload) (*fleet.Us
if license == nil {
return nil, ctxerr.New(ctx, "license not found")
}
if err := fleet.ValidateRoleForLicense(p.GlobalRole, p.Teams, *license); err != nil {
if err := fleet.ValidateUserRoles(true, p, *license); err != nil {
return nil, ctxerr.Wrap(ctx, err, "validate role")
}

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"reflect"
"strconv"
"github.com/fleetdm/fleet/v4/server/authz"
@ -201,14 +202,6 @@ func deleteTeamPoliciesEndpoint(ctx context.Context, request interface{}, svc fl
}
func (svc Service) DeleteTeamPolicies(ctx context.Context, teamID uint, ids []uint) ([]uint, error) {
// First check if authorized to read policies
if err := svc.authz.Authorize(ctx, &fleet.Policy{
PolicyData: fleet.PolicyData{
TeamID: ptr.Uint(teamID),
},
}, fleet.ActionRead); err != nil {
return nil, err
}
if len(ids) == 0 {
return nil, nil
}
@ -217,7 +210,6 @@ func (svc Service) DeleteTeamPolicies(ctx context.Context, teamID uint, ids []ui
return nil, ctxerr.Wrap(ctx, err, "getting policies by ID")
}
// Then check if authorized to write policies
if err := svc.authz.Authorize(ctx, &fleet.Policy{
PolicyData: fleet.PolicyData{
TeamID: ptr.Uint(teamID),
@ -289,22 +281,29 @@ func (svc *Service) ModifyTeamPolicy(ctx context.Context, teamID uint, id uint,
return svc.modifyPolicy(ctx, &teamID, id, p)
}
func checkTeamID(teamID *uint, policy *fleet.Policy) bool {
return policy != nil && reflect.DeepEqual(teamID, policy.TeamID)
}
func (svc *Service) modifyPolicy(ctx context.Context, teamID *uint, id uint, p fleet.ModifyPolicyPayload) (*fleet.Policy, error) {
// First make sure the user can read the policies.
if err := svc.authz.Authorize(ctx, &fleet.Policy{
PolicyData: fleet.PolicyData{
TeamID: teamID,
},
}, fleet.ActionRead); err != nil {
}, fleet.ActionWrite); err != nil {
return nil, err
}
policy, err := svc.ds.Policy(ctx, id)
if err != nil {
return nil, err
}
// Then we make sure they can modify the team's policies.
if err := svc.authz.Authorize(ctx, policy, fleet.ActionWrite); err != nil {
return nil, err
if ok := checkTeamID(teamID, policy); !ok {
return nil, ctxerr.Wrap(ctx, &fleet.BadRequestError{
Message: "policy does not belong to team/global",
InternalErr: fmt.Errorf("teamID: %+v, policy: %+v", teamID, policy),
})
}
if err := p.Verify(); err != nil {

View File

@ -4,9 +4,11 @@ import (
"context"
"database/sql"
"encoding/json"
"errors"
"testing"
"time"
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"
@ -367,3 +369,23 @@ func TestApplyTeamSpecs(t *testing.T) {
}
})
}
// 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.ApplySpecOptions{})
require.Error(t, err)
az, ok := authz_ctx.FromContext(ctx)
require.True(t, ok)
require.True(t, az.Checked())
}

View File

@ -12,6 +12,7 @@ import (
"net/url"
"os"
"regexp"
"sync"
"testing"
"time"
@ -59,7 +60,11 @@ type withServer struct {
users map[string]fleet.User
token string
cachedAdminToken string
lq *live_query_mock.MockLiveQuery
cachedTokensMu sync.Mutex
cachedTokens map[string]string // email -> auth token
lq *live_query_mock.MockLiveQuery
}
func (ts *withServer) SetupSuite(dbName string) {
@ -75,6 +80,7 @@ func (ts *withServer) SetupSuite(dbName string) {
})
ts.server = server
ts.users = users
ts.cachedTokens = make(map[string]string)
ts.token = ts.getTestAdminToken()
ts.cachedAdminToken = ts.token
}
@ -215,6 +221,20 @@ func (ts *withServer) getTestAdminToken() string {
return ts.cachedAdminToken
}
// getCachedUserToken returns the cached auth token for the given test user email.
// If it's not found, then a login request is performed and the token cached.
func (ts *withServer) getCachedUserToken(email, password string) string {
ts.cachedTokensMu.Lock()
defer ts.cachedTokensMu.Unlock()
token, ok := ts.cachedTokens[email]
if !ok {
token = ts.getTestToken(email, password)
ts.cachedTokens[email] = token
}
return token
}
func (ts *withServer) getTestToken(email string, password string) string {
params := loginRequest{
Email: email,

View File

@ -303,7 +303,7 @@ func (svc *Service) ModifyUser(ctx context.Context, userID uint, p fleet.UserPay
if license == nil {
return nil, ctxerr.New(ctx, "license not found")
}
if err := fleet.ValidateRoleForLicense(p.GlobalRole, p.Teams, *license); err != nil {
if err := fleet.ValidateUserRoles(false, p, *license); err != nil {
return nil, ctxerr.Wrap(ctx, err, "validate role")
}
}

View File

@ -113,4 +113,26 @@ var (
},
},
}
UserGitOps = &fleet.User{
ID: 15,
GlobalRole: ptr.String(fleet.RoleGitOps),
}
UserTeamGitOpsTeam1 = &fleet.User{
ID: 16,
Teams: []fleet.UserTeam{
{
Team: fleet.Team{ID: 1},
Role: fleet.RoleGitOps,
},
},
}
UserTeamGitOpsTeam2 = &fleet.User{
ID: 17,
Teams: []fleet.UserTeam{
{
Team: fleet.Team{ID: 2},
Role: fleet.RoleGitOps,
},
},
}
)