New observer_plus role (#10675)

#8593

This PR adds a new role `observer_plus` to Fleet. (The `GitOps` role
will be added on a separate PR.)

- [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-05 15:23:49 -03:00 committed by GitHub
parent 1516caad46
commit a756614c1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 434 additions and 226 deletions

View File

@ -0,0 +1 @@
* Add `observer_plus` user role to Fleet. Observers+ are observers that can run any live query.

View File

@ -3056,7 +3056,7 @@ Fleet will attempt to parse SAML custom attributes with the following format:
- `FLEET_JIT_USER_ROLE_GLOBAL`: Specifies the global role to use when creating the user.
- `FLEET_JIT_USER_ROLE_TEAM_<TEAM_ID>`: Specifies team role for team with ID `<TEAM_ID>` to use when creating the user.
Currently supported values for the above attributes are: `admin`, `maintainer` and `observer`.
Currently supported values for the above attributes are: `admin`, `maintainer`, `observer` and `observer_plus`.
SAML supports multi-valued attributes, Fleet will always use the last value.
NOTE: Setting both `FLEET_JIT_USER_ROLE_GLOBAL` and `FLEET_JIT_USER_ROLE_TEAM_<TEAM_ID>` will cause an error during login as Fleet users cannot be Global users and belong to teams.

View File

@ -6,49 +6,49 @@ Users with the Admin role receive all permissions.
## User permissions
| **Action** | 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 |
| ------------------------------------------------------------------------------------------------------------------------------------------ | -------- | --------- | ---------- | ----- |
| 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 +71,34 @@ 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 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 |
| -------------------------------------------------------------------------------------------------------------------------------- | ------------- | -------------- | --------------- | ---------- |
| 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 | ✅ | ✅ | ✅ | ✅ |
\* Applies only to [Fleet REST API](https://fleetdm.com/docs/using-fleet/rest-api)

View File

@ -4303,7 +4303,7 @@ Returns a list of all queries in the Fleet instance.
| name | string | body | **Required**. The name of the query. |
| query | string | body | **Required**. The query in SQL syntax. |
| description | string | body | The query's description. |
| observer_can_run | bool | body | Whether or not users with the `observer` role can run the query. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). |
| observer_can_run | bool | body | Whether or not users with the `observer` role can run the query. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). This field is only relevant for the `observer` role. The `observer_plus` role can run any query and is not limited by this flag (`observer_plus` role was added in Fleet 4.30.0). |
#### Example
@ -4356,7 +4356,7 @@ Returns the query specified by ID.
| name | string | body | The name of the query. |
| query | string | body | The query in SQL syntax. |
| description | string | body | The query's description. |
| observer_can_run | bool | body | Whether or not users with the `observer` role can run the query. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). |
| observer_can_run | bool | body | Whether or not users with the `observer` role can run the query. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). This field is only relevant for the `observer` role. The `observer_plus` role can run any query and is not limited by this flag (`observer_plus` role was added in Fleet 4.30.0). |
#### Example
@ -6000,8 +6000,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`). 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`). 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, 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. |
#### Example
@ -6117,9 +6117,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`). 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, the `observer_plus` role was introduced. If `global_role` is specified, `teams` cannot be specified. |
| 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`). 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, the `observer_plus` role was introduced. If `teams` is specified, `global_role` cannot be specified. |
#### Example

View File

@ -18,8 +18,9 @@ write := "write"
write_role := "write_role"
change_password := "change_password"
# Query specific actions
# Action used on object "targeted_query" used for running live queries.
run := "run"
# Action used on object "query" used for running "new" live queries.
run_new := "run_new"
# MDM specific actions
@ -29,6 +30,7 @@ mdm_command := "mdm_command"
admin := "admin"
maintainer := "maintainer"
observer := "observer"
observer_plus := "observer_plus"
# Default deny
default allow = false
@ -75,14 +77,14 @@ allow {
allow {
object.type == "team"
object.id != 0
team_role(subject, object.id) == [admin,maintainer,observer][_]
team_role(subject, object.id) == [admin, maintainer, observer, observer_plus][_]
action == read
}
# Global users can read all teams.
allow {
object.type == "team"
object.id != 0
subject.global_role == [admin, maintainer, observer][_]
subject.global_role == [admin, maintainer, observer, observer_plus][_]
action == read
}
@ -212,24 +214,24 @@ allow {
action == [read, write][_]
}
# Allow read for global observer
# Allow read for global observer and observer_plus.
allow {
object.type == "host"
subject.global_role = observer
subject.global_role == [observer, observer_plus][_]
action == read
}
# Allow read for matching team admin/maintainer/observer
# Allow read for matching team admin/maintainer/observer/observer_plus.
allow {
object.type == "host"
team_role(subject, object.team_id) == [admin, maintainer, observer][_]
team_role(subject, object.team_id) == [admin, maintainer, observer, observer_plus][_]
action == read
}
# Team admins and maintainers can write to hosts of their own team
allow {
object.type == "host"
team_role(subject, object.team_id) == [admin,maintainer][_]
team_role(subject, object.team_id) == [admin, maintainer][_]
action == write
}
@ -270,12 +272,7 @@ allow {
# Global admins and maintainers can write queries
allow {
object.type == "query"
subject.global_role == admin
action == write
}
allow {
object.type == "query"
subject.global_role == maintainer
subject.global_role == [admin, maintainer][_]
action == write
}
@ -295,29 +292,19 @@ allow {
action == write
}
# Global admins and maintainers can run any
# Global admins, maintainers and observer_plus can run any query (saved and new).
allow {
object.type == "targeted_query"
subject.global_role == admin
action = run
}
allow {
object.type == "targeted_query"
subject.global_role == maintainer
subject.global_role == [admin, maintainer, observer_plus][_]
action = run
}
allow {
object.type == "query"
subject.global_role == admin
action = run_new
}
allow {
object.type == "query"
subject.global_role == maintainer
subject.global_role == [admin, maintainer, observer_plus][_]
action = run_new
}
# Team admin and maintainer running a non-observers_can_run query must have the targets
# Team admin, maintainer and observer_plus running a non-observers_can_run query must have the targets
# filtered to only teams that they maintain.
allow {
object.type == "targeted_query"
@ -326,34 +313,33 @@ allow {
action == run
not is_null(object.host_targets.teams)
ok_teams := { tmid | tmid := object.host_targets.teams[_]; team_role(subject, tmid) == [admin,maintainer][_] }
ok_teams := { tmid | tmid := object.host_targets.teams[_]; team_role(subject, tmid) == [admin, maintainer, observer_plus][_] }
count(ok_teams) == count(object.host_targets.teams)
}
# Team admin and maintainer running a non-observers_can_run query when no target teams
# are specified.
# Team admin, maintainer and observer_plus running a non-observers_can_run query when no target teams are specified.
allow {
object.type == "targeted_query"
object.observer_can_run == false
is_null(subject.global_role)
action == run
# If role is admin or maintainer on any team
team_role(subject, subject.teams[_].id) == [admin,maintainer][_]
# If role is admin, maintainer or observer_plus on any team.
team_role(subject, subject.teams[_].id) == [admin, maintainer, observer_plus][_]
# and there are no team targets
is_null(object.host_targets.teams)
}
# Team admin and maintainer can run a new query
# Team admin, maintainer and observer_plus can run a new query.
allow {
object.type == "query"
# If role is admin or maintainer on any team
team_role(subject, subject.teams[_].id) == [admin,maintainer][_]
# If role is admin, maintainer or observer_plus on any team.
team_role(subject, subject.teams[_].id) == [admin, maintainer, observer_plus][_]
action == run_new
}
# Observers can run only if observers_can_run
# Global observers can run only if observers_can_run.
allow {
object.type == "targeted_query"
object.observer_can_run == true
@ -361,7 +347,7 @@ allow {
action = run
}
# Team observer running a observers_can_run query must have the targets
# Team admin, maintainer, observer_plus and observer running a observers_can_run query must have the targets
# filtered to only teams that they observe.
allow {
object.type == "targeted_query"
@ -370,20 +356,19 @@ allow {
action == run
not is_null(object.host_targets.teams)
ok_teams := { tmid | tmid := object.host_targets.teams[_]; team_role(subject, tmid) == [admin,maintainer,observer][_] }
ok_teams := { tmid | tmid := object.host_targets.teams[_]; team_role(subject, tmid) == [admin, maintainer, observer_plus, observer][_] }
count(ok_teams) == count(object.host_targets.teams)
}
# Team observer running a observers_can_run query and there are no
# target teams.
# Team admin, maintainer, observer_plus and observer running a observers_can_run query and there are no target teams.
allow {
object.type == "targeted_query"
object.observer_can_run == true
is_null(subject.global_role)
action == run
# If role is admin, maintainer or observer on any team
team_role(subject, subject.teams[_].id) == [admin,maintainer,observer][_]
# If role is admin, maintainer, observer_plus or observer on any team.
team_role(subject, subject.teams[_].id) == [admin, maintainer, observer_plus, observer][_]
# and there are no team targets
is_null(object.host_targets.teams)
@ -420,13 +405,13 @@ allow {
action == read
}
# Team admins, maintainers and observers can read their team's pack.
# Team admins, maintainers, observers and observer_plus can read their team's pack.
#
# NOTE: Action "read" on a team's pack includes listing its scheduled queries.
allow {
object.type == "pack"
not is_null(object.pack_team_id)
team_role(subject, object.pack_team_id) == [admin, maintainer, observer][_]
team_role(subject, object.pack_team_id) == [admin, maintainer, observer, observer_plus][_]
action == read
}
@ -464,10 +449,10 @@ allow {
action == [read, write][_]
}
# Global Observer can read any policies
# Global observer and observer_plus can read any policies
allow {
object.type == "policy"
subject.global_role == observer
subject.global_role == [observer, observer_plus][_]
action == read
}
@ -479,19 +464,19 @@ allow {
action == [read, write][_]
}
# Team admin, maintainers and observers can read global policies
# Team admin, maintainers, observers and observer_plus can read global policies.
allow {
is_null(object.team_id)
object.type == "policy"
team_role(subject, subject.teams[_].id) == [admin,maintainer,observer][_]
team_role(subject, subject.teams[_].id) == [admin, maintainer, observer, observer_plus][_]
action == read
}
# Team Observer can read policies for their teams
# Team observer and observer_plus can read policies for their teams.
allow {
not is_null(object.team_id)
object.type == "policy"
team_role(subject, object.team_id) == observer
team_role(subject, object.team_id) == [observer, observer_plus][_]
action == read
}
@ -502,7 +487,7 @@ allow {
# Global users can read all software.
allow {
object.type == "software_inventory"
subject.global_role == [admin, maintainer, observer][_]
subject.global_role == [admin, maintainer, observer, observer_plus][_]
action == read
}
@ -510,7 +495,7 @@ allow {
allow {
not is_null(object.team_id)
object.type == "software_inventory"
team_role(subject, object.team_id) == [admin, maintainer, observer][_]
team_role(subject, object.team_id) == [admin, maintainer, observer, observer_plus][_]
action == read
}
@ -578,18 +563,18 @@ allow {
action == write
}
# Admin, maintainer and observer can read MDM Apple commands.
# Global admins, maintainers, observers and observer_plus can read MDM Apple commands.
allow {
object.type == "mdm_apple_command"
subject.global_role == [admin, maintainer, observer][_]
subject.global_role == [admin, maintainer, observer, observer_plus][_]
action == read
}
# Team admins, maintainers and observers can read MDM Apple commands on hosts of their teams.
# Team admins, maintainers, observers and observer_plus can read MDM Apple commands on hosts of their teams.
allow {
not is_null(object.team_id)
object.type == "mdm_apple_command"
team_role(subject, object.team_id) == [admin, maintainer, observer][_]
team_role(subject, object.team_id) == [admin, maintainer, observer, observer_plus][_]
action == read
}

View File

@ -60,6 +60,9 @@ func TestAuthorizeAppConfig(t *testing.T) {
{user: test.UserObserver, object: config, action: read, allow: true},
{user: test.UserObserver, object: config, action: write, allow: false},
{user: test.UserObserverPlus, object: config, action: read, allow: true},
{user: test.UserObserverPlus, object: config, action: write, allow: false},
})
}
@ -90,6 +93,11 @@ func TestAuthorizeSession(t *testing.T) {
{user: test.UserObserver, object: session, action: write, allow: false},
{user: test.UserObserver, object: &fleet.Session{UserID: test.UserObserver.ID}, action: read, allow: true},
{user: test.UserObserver, object: &fleet.Session{UserID: test.UserObserver.ID}, action: write, allow: true},
{user: test.UserObserverPlus, object: session, action: read, allow: false},
{user: test.UserObserverPlus, object: session, action: write, allow: false},
{user: test.UserObserverPlus, object: &fleet.Session{UserID: test.UserObserverPlus.ID}, action: read, allow: true},
{user: test.UserObserverPlus, object: &fleet.Session{UserID: test.UserObserverPlus.ID}, action: write, allow: true},
})
}
@ -167,6 +175,19 @@ func TestAuthorizeUser(t *testing.T) {
{user: test.UserObserver, object: test.UserObserver, action: writeRole, allow: false},
{user: test.UserObserver, object: test.UserObserver, action: changePwd, allow: true},
// Global observers+ cannot read/write users.
{user: test.UserObserverPlus, object: user, action: read, allow: false},
{user: test.UserObserverPlus, object: user, action: write, allow: false},
{user: test.UserObserverPlus, object: user, action: writeRole, allow: false},
{user: test.UserObserverPlus, object: user, action: changePwd, allow: false},
// Global observers+ cannot create users.
{user: test.UserObserverPlus, object: newUser, action: write, allow: false},
// Global observers+ can read/write itself (besides roles).
{user: test.UserObserverPlus, object: test.UserObserverPlus, action: read, allow: true},
{user: test.UserObserverPlus, object: test.UserObserverPlus, action: write, allow: true},
{user: test.UserObserverPlus, object: test.UserObserverPlus, action: writeRole, allow: false},
{user: test.UserObserverPlus, object: test.UserObserverPlus, action: changePwd, allow: true},
// Team admins cannot read/write global users.
{user: teamAdmin, object: user, action: read, allow: false},
{user: teamAdmin, object: user, action: write, allow: false},
@ -208,6 +229,9 @@ func TestAuthorizeInvite(t *testing.T) {
{user: test.UserObserver, object: invite, action: read, allow: false},
{user: test.UserObserver, object: invite, action: write, allow: false},
{user: test.UserObserverPlus, object: invite, action: read, allow: false},
{user: test.UserObserverPlus, object: invite, action: write, allow: false},
})
}
@ -246,6 +270,10 @@ func TestAuthorizeEnrollSecret(t *testing.T) {
{user: test.UserObserver, object: globalSecret, action: write, allow: false},
{user: test.UserObserver, object: teamSecret, action: read, allow: false},
{user: test.UserObserver, object: teamSecret, action: write, allow: false},
{user: test.UserObserverPlus, object: globalSecret, action: read, allow: false},
{user: test.UserObserverPlus, object: globalSecret, action: write, allow: false},
{user: test.UserObserverPlus, object: teamSecret, action: read, allow: false},
{user: test.UserObserverPlus, object: teamSecret, action: write, allow: false},
{user: teamObserver, object: globalSecret, action: read, allow: false},
{user: teamObserver, object: globalSecret, action: write, allow: false},
{user: teamObserver, object: teamSecret, action: read, allow: false},
@ -296,6 +324,9 @@ func TestAuthorizeTeam(t *testing.T) {
{user: test.UserObserver, object: team, action: read, allow: true},
{user: test.UserObserver, object: team, action: write, allow: false},
{user: test.UserObserverPlus, object: team, action: read, allow: true},
{user: test.UserObserverPlus, object: team, action: write, allow: false},
})
}
@ -318,6 +349,9 @@ func TestAuthorizeLabel(t *testing.T) {
{user: test.UserObserver, object: label, action: read, allow: true},
{user: test.UserObserver, object: label, action: write, allow: false},
{user: test.UserObserverPlus, object: label, action: read, allow: true},
{user: test.UserObserverPlus, object: label, action: write, allow: false},
})
}
@ -379,6 +413,18 @@ func TestAuthorizeHost(t *testing.T) {
{user: test.UserObserver, object: hostTeam2, action: write, allow: false},
{user: test.UserObserver, object: hostTeam2, action: mdmCommand, allow: false},
// Global observer+ can read all
{user: test.UserObserverPlus, object: host, action: read, allow: true},
{user: test.UserObserverPlus, object: host, action: write, allow: false},
{user: test.UserObserverPlus, object: host, action: list, allow: true},
{user: test.UserObserverPlus, object: host, action: mdmCommand, allow: false},
{user: test.UserObserverPlus, object: hostTeam1, action: read, allow: true},
{user: test.UserObserverPlus, object: hostTeam1, action: write, allow: false},
{user: test.UserObserverPlus, object: hostTeam1, action: mdmCommand, allow: false},
{user: test.UserObserverPlus, object: hostTeam2, action: read, allow: true},
{user: test.UserObserverPlus, object: hostTeam2, action: write, allow: false},
{user: test.UserObserverPlus, object: hostTeam2, action: mdmCommand, allow: false},
// Global admin can read/write all
{user: test.UserAdmin, object: host, action: read, allow: true},
{user: test.UserAdmin, object: host, action: write, allow: true},
@ -469,6 +515,12 @@ func TestAuthorizeQuery(t *testing.T) {
{Team: fleet.Team{ID: 2}, Role: fleet.RoleObserver},
},
}
teamObserverPlus := &fleet.User{
ID: 104,
Teams: []fleet.UserTeam{
{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserverPlus},
},
}
query := &fleet.Query{ObserverCanRun: false}
emptyTquery := &fleet.TargetedQuery{Query: query}
@ -529,7 +581,21 @@ func TestAuthorizeQuery(t *testing.T) {
{user: test.UserObserver, object: team12ObsQuery, action: run, allow: true}, // can run observer query
{user: test.UserObserver, object: observerQuery, action: runNew, allow: false},
// Global maintainer can read/write (even not authored by them)/run any
// Global observer+ can read all queries, not write them, and can run any query.
{user: test.UserObserverPlus, object: query, action: read, allow: true},
{user: test.UserObserverPlus, object: query, action: write, allow: false},
{user: test.UserObserverPlus, object: teamAdminQuery, action: write, allow: false},
{user: test.UserObserverPlus, object: emptyTquery, action: run, allow: true},
{user: test.UserObserverPlus, object: team1Query, action: run, allow: true},
{user: test.UserObserverPlus, object: query, action: runNew, allow: true},
{user: test.UserObserverPlus, object: observerQuery, action: read, allow: true},
{user: test.UserObserverPlus, object: observerQuery, action: write, allow: false},
{user: test.UserObserverPlus, object: emptyTobsQuery, action: run, allow: true}, // can run observer query
{user: test.UserObserverPlus, object: team1ObsQuery, action: run, allow: true}, // can run observer query
{user: test.UserObserverPlus, object: team12ObsQuery, action: run, allow: true}, // can run observer query
{user: test.UserObserverPlus, object: observerQuery, action: runNew, allow: true},
// Global maintainer can read/write (even not authored by them)/run any.
{user: test.UserMaintainer, object: query, action: read, allow: true},
{user: test.UserMaintainer, object: query, action: write, allow: true},
{user: test.UserMaintainer, object: teamMaintQuery, action: write, allow: true},
@ -572,6 +638,21 @@ func TestAuthorizeQuery(t *testing.T) {
{user: teamObserver, object: team2ObsQuery, action: run, allow: false}, // not filtered only to observed teams
{user: teamObserver, object: observerQuery, action: runNew, allow: false},
// Team observer+ can read all queries, not write them, and can run any query.
{user: teamObserverPlus, object: query, action: read, allow: true},
{user: teamObserverPlus, object: query, action: write, allow: false},
{user: teamObserverPlus, object: teamAdminQuery, action: write, allow: false},
{user: teamObserverPlus, object: emptyTquery, action: run, allow: true},
{user: teamObserverPlus, object: team1Query, action: run, allow: true},
{user: teamObserverPlus, object: query, action: runNew, allow: true},
{user: teamObserverPlus, object: observerQuery, action: read, allow: true},
{user: teamObserverPlus, object: observerQuery, action: write, allow: false},
{user: teamObserverPlus, object: emptyTobsQuery, action: run, allow: true}, // can run observer query with no targeted team
{user: teamObserverPlus, object: team1ObsQuery, action: run, allow: true}, // can run observer query filtered to observed team
{user: teamObserverPlus, object: team12ObsQuery, action: run, allow: false}, // not filtered only to observed teams
{user: teamObserverPlus, object: team2ObsQuery, action: run, allow: false}, // not filtered only to observed teams
{user: teamObserverPlus, object: observerQuery, action: runNew, allow: true},
// Team maintainer can read/write their own queries/run queries filtered on their team(s)
{user: teamMaintainer, object: query, action: read, allow: true},
{user: teamMaintainer, object: query, action: write, allow: true},
@ -645,6 +726,7 @@ func TestAuthorizeTargets(t *testing.T) {
{user: test.UserAdmin, object: target, action: read, allow: true},
{user: test.UserMaintainer, object: target, action: read, allow: true},
{user: test.UserObserver, object: target, action: read, allow: true},
{user: test.UserObserverPlus, object: target, action: read, allow: true},
})
}
@ -667,6 +749,9 @@ func TestAuthorizePacks(t *testing.T) {
{user: test.UserObserver, object: pack, action: read, allow: false},
{user: test.UserObserver, object: pack, action: write, allow: false},
{user: test.UserObserverPlus, object: pack, action: read, allow: false},
{user: test.UserObserverPlus, object: pack, action: write, allow: false},
})
}
@ -756,6 +841,8 @@ func TestAuthorizeCarves(t *testing.T) {
{user: test.UserMaintainer, object: carve, action: write, allow: false},
{user: test.UserObserver, object: carve, action: read, allow: false},
{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},
// Only admins allowed
{user: test.UserAdmin, object: carve, action: read, allow: true},
@ -767,7 +854,7 @@ func TestAuthorizePolicies(t *testing.T) {
t.Parallel()
globalPolicy := &fleet.Policy{}
teamPolicy := &fleet.Policy{
team1Policy := &fleet.Policy{
PolicyData: fleet.PolicyData{
TeamID: ptr.Uint(1),
},
@ -782,27 +869,34 @@ func TestAuthorizePolicies(t *testing.T) {
{user: test.UserObserver, object: globalPolicy, action: write, allow: false},
{user: test.UserObserver, object: globalPolicy, action: read, allow: true},
{user: test.UserAdmin, object: teamPolicy, action: write, allow: true},
{user: test.UserAdmin, object: teamPolicy, action: read, allow: true},
{user: test.UserMaintainer, object: teamPolicy, action: write, allow: true},
{user: test.UserMaintainer, object: teamPolicy, action: read, allow: true},
{user: test.UserObserver, object: teamPolicy, action: write, allow: false},
{user: test.UserObserver, object: teamPolicy, action: read, allow: true},
{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.UserTeamAdminTeam1, object: teamPolicy, action: write, allow: true},
{user: test.UserTeamAdminTeam1, object: teamPolicy, action: read, allow: true},
{user: test.UserTeamAdminTeam2, object: teamPolicy, action: write, allow: false},
{user: test.UserTeamAdminTeam2, object: teamPolicy, 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: teamPolicy, action: write, allow: true},
{user: test.UserTeamMaintainerTeam1, object: teamPolicy, action: read, allow: true},
{user: test.UserTeamMaintainerTeam2, object: teamPolicy, action: write, allow: false},
{user: test.UserTeamMaintainerTeam2, object: teamPolicy, 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: teamPolicy, action: write, allow: false},
{user: test.UserTeamObserverTeam1, object: teamPolicy, action: read, allow: true},
{user: test.UserTeamObserverTeam2, object: teamPolicy, action: write, allow: false},
{user: test.UserTeamObserverTeam2, object: teamPolicy, 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},
@ -815,59 +909,74 @@ func TestAuthorizeMDMAppleConfigProfile(t *testing.T) {
t.Parallel()
globalProfile := &fleet.MDMAppleConfigProfile{}
teamProfile := &fleet.MDMAppleConfigProfile{
team1Profile := &fleet.MDMAppleConfigProfile{
TeamID: ptr.Uint(1),
}
runTestCases(t, []authTestCase{
{user: test.UserNoRoles, object: globalProfile, action: write, allow: false},
{user: test.UserNoRoles, object: globalProfile, action: read, allow: false},
{user: test.UserNoRoles, object: teamProfile, action: write, allow: false},
{user: test.UserNoRoles, object: teamProfile, action: read, allow: false},
{user: test.UserNoRoles, object: team1Profile, action: write, allow: false},
{user: test.UserNoRoles, object: team1Profile, action: read, allow: false},
{user: test.UserAdmin, object: globalProfile, action: write, allow: true},
{user: test.UserAdmin, object: globalProfile, action: read, allow: true},
{user: test.UserAdmin, object: teamProfile, action: write, allow: true},
{user: test.UserAdmin, object: teamProfile, action: read, allow: true},
{user: test.UserAdmin, object: team1Profile, action: write, allow: true},
{user: test.UserAdmin, object: team1Profile, action: read, allow: true},
{user: test.UserMaintainer, object: globalProfile, action: write, allow: true},
{user: test.UserMaintainer, object: globalProfile, action: read, allow: true},
{user: test.UserMaintainer, object: teamProfile, action: write, allow: true},
{user: test.UserMaintainer, object: teamProfile, action: read, allow: true},
{user: test.UserMaintainer, object: team1Profile, action: write, allow: true},
{user: test.UserMaintainer, object: team1Profile, action: read, allow: true},
{user: test.UserObserver, object: globalProfile, action: write, allow: false},
{user: test.UserObserver, object: globalProfile, action: read, allow: false},
{user: test.UserObserver, object: teamProfile, action: write, allow: false},
{user: test.UserObserver, object: teamProfile, action: read, allow: false},
{user: test.UserObserver, object: team1Profile, action: write, allow: false},
{user: test.UserObserver, object: team1Profile, action: read, allow: false},
{user: test.UserObserverPlus, object: globalProfile, action: write, allow: false},
{user: test.UserObserverPlus, object: globalProfile, action: read, allow: false},
{user: test.UserObserverPlus, object: team1Profile, action: write, allow: false},
{user: test.UserObserverPlus, 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: teamProfile, action: write, allow: true},
{user: test.UserTeamAdminTeam1, object: teamProfile, action: read, allow: true},
{user: test.UserTeamAdminTeam1, object: team1Profile, action: write, allow: true},
{user: test.UserTeamAdminTeam1, object: team1Profile, action: read, allow: true},
{user: test.UserTeamAdminTeam2, object: globalProfile, action: write, allow: false},
{user: test.UserTeamAdminTeam2, object: globalProfile, action: read, allow: false},
{user: test.UserTeamAdminTeam2, object: teamProfile, action: write, allow: false},
{user: test.UserTeamAdminTeam2, object: teamProfile, action: read, allow: false},
{user: test.UserTeamAdminTeam2, object: team1Profile, action: write, allow: false},
{user: test.UserTeamAdminTeam2, object: team1Profile, action: read, allow: false},
{user: test.UserTeamMaintainerTeam1, object: globalProfile, action: write, allow: false},
{user: test.UserTeamMaintainerTeam1, object: globalProfile, action: read, allow: false},
{user: test.UserTeamMaintainerTeam1, object: teamProfile, action: write, allow: true},
{user: test.UserTeamMaintainerTeam1, object: teamProfile, action: read, allow: true},
{user: test.UserTeamMaintainerTeam1, object: team1Profile, action: write, allow: true},
{user: test.UserTeamMaintainerTeam1, object: team1Profile, action: read, allow: true},
{user: test.UserTeamMaintainerTeam2, object: globalProfile, action: write, allow: false},
{user: test.UserTeamMaintainerTeam2, object: globalProfile, action: read, allow: false},
{user: test.UserTeamMaintainerTeam2, object: teamProfile, action: write, allow: false},
{user: test.UserTeamMaintainerTeam2, object: teamProfile, action: read, allow: false},
{user: test.UserTeamMaintainerTeam2, object: team1Profile, action: write, allow: false},
{user: test.UserTeamMaintainerTeam2, object: team1Profile, action: read, allow: false},
{user: test.UserTeamObserverTeam1, object: globalProfile, action: write, allow: false},
{user: test.UserTeamObserverTeam1, object: globalProfile, action: read, allow: false},
{user: test.UserTeamObserverTeam1, object: teamProfile, action: write, allow: false},
{user: test.UserTeamObserverTeam1, object: teamProfile, action: read, allow: false},
{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: teamProfile, action: write, allow: false},
{user: test.UserTeamObserverTeam2, object: teamProfile, action: read, allow: false},
{user: test.UserTeamObserverTeam2, object: team1Profile, action: write, allow: false},
{user: test.UserTeamObserverTeam2, 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},
})
}
@ -875,59 +984,74 @@ func TestAuthorizeMDMAppleSettings(t *testing.T) {
t.Parallel()
globalSettings := &fleet.MDMAppleSettingsPayload{}
teamSettings := &fleet.MDMAppleSettingsPayload{
team1Settings := &fleet.MDMAppleSettingsPayload{
TeamID: ptr.Uint(1),
}
runTestCases(t, []authTestCase{
{user: test.UserNoRoles, object: globalSettings, action: write, allow: false},
{user: test.UserNoRoles, object: globalSettings, action: read, allow: false},
{user: test.UserNoRoles, object: teamSettings, action: write, allow: false},
{user: test.UserNoRoles, object: teamSettings, action: read, allow: false},
{user: test.UserNoRoles, object: team1Settings, action: write, allow: false},
{user: test.UserNoRoles, object: team1Settings, action: read, allow: false},
{user: test.UserAdmin, object: globalSettings, action: write, allow: true},
{user: test.UserAdmin, object: globalSettings, action: read, allow: true},
{user: test.UserAdmin, object: teamSettings, action: write, allow: true},
{user: test.UserAdmin, object: teamSettings, action: read, allow: true},
{user: test.UserAdmin, object: team1Settings, action: write, allow: true},
{user: test.UserAdmin, object: team1Settings, action: read, allow: true},
{user: test.UserMaintainer, object: globalSettings, action: write, allow: true},
{user: test.UserMaintainer, object: globalSettings, action: read, allow: true},
{user: test.UserMaintainer, object: teamSettings, action: write, allow: true},
{user: test.UserMaintainer, object: teamSettings, action: read, allow: true},
{user: test.UserMaintainer, object: team1Settings, action: write, allow: true},
{user: test.UserMaintainer, object: team1Settings, action: read, allow: true},
{user: test.UserObserver, object: globalSettings, action: write, allow: false},
{user: test.UserObserver, object: globalSettings, action: read, allow: false},
{user: test.UserObserver, object: teamSettings, action: write, allow: false},
{user: test.UserObserver, object: teamSettings, action: read, allow: false},
{user: test.UserObserver, object: team1Settings, action: write, allow: false},
{user: test.UserObserver, object: team1Settings, action: read, allow: false},
{user: test.UserObserverPlus, object: globalSettings, action: write, allow: false},
{user: test.UserObserverPlus, object: globalSettings, action: read, allow: false},
{user: test.UserObserverPlus, object: team1Settings, action: write, allow: false},
{user: test.UserObserverPlus, 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: teamSettings, action: write, allow: true},
{user: test.UserTeamAdminTeam1, object: teamSettings, action: read, allow: true},
{user: test.UserTeamAdminTeam1, object: team1Settings, action: write, allow: true},
{user: test.UserTeamAdminTeam1, object: team1Settings, action: read, allow: true},
{user: test.UserTeamAdminTeam2, object: globalSettings, action: write, allow: false},
{user: test.UserTeamAdminTeam2, object: globalSettings, action: read, allow: false},
{user: test.UserTeamAdminTeam2, object: teamSettings, action: write, allow: false},
{user: test.UserTeamAdminTeam2, object: teamSettings, action: read, allow: false},
{user: test.UserTeamAdminTeam2, object: team1Settings, action: write, allow: false},
{user: test.UserTeamAdminTeam2, object: team1Settings, action: read, allow: false},
{user: test.UserTeamMaintainerTeam1, object: globalSettings, action: write, allow: false},
{user: test.UserTeamMaintainerTeam1, object: globalSettings, action: read, allow: false},
{user: test.UserTeamMaintainerTeam1, object: teamSettings, action: write, allow: true},
{user: test.UserTeamMaintainerTeam1, object: teamSettings, action: read, allow: true},
{user: test.UserTeamMaintainerTeam1, object: team1Settings, action: write, allow: true},
{user: test.UserTeamMaintainerTeam1, object: team1Settings, action: read, allow: true},
{user: test.UserTeamMaintainerTeam2, object: globalSettings, action: write, allow: false},
{user: test.UserTeamMaintainerTeam2, object: globalSettings, action: read, allow: false},
{user: test.UserTeamMaintainerTeam2, object: teamSettings, action: write, allow: false},
{user: test.UserTeamMaintainerTeam2, object: teamSettings, action: read, allow: false},
{user: test.UserTeamMaintainerTeam2, object: team1Settings, action: write, allow: false},
{user: test.UserTeamMaintainerTeam2, object: team1Settings, action: read, allow: false},
{user: test.UserTeamObserverTeam1, object: globalSettings, action: write, allow: false},
{user: test.UserTeamObserverTeam1, object: globalSettings, action: read, allow: false},
{user: test.UserTeamObserverTeam1, object: teamSettings, action: write, allow: false},
{user: test.UserTeamObserverTeam1, object: teamSettings, action: read, allow: false},
{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: teamSettings, action: write, allow: false},
{user: test.UserTeamObserverTeam2, object: teamSettings, action: read, allow: false},
{user: test.UserTeamObserverTeam2, object: team1Settings, action: write, allow: false},
{user: test.UserTeamObserverTeam2, 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},
})
}

View File

@ -859,15 +859,13 @@ func (ds *Datastore) whereFilterHostsByTeams(filter fleet.TeamFilter, hostKey st
if filter.User.GlobalRole != nil {
switch *filter.User.GlobalRole {
case fleet.RoleAdmin, fleet.RoleMaintainer:
case fleet.RoleAdmin, fleet.RoleMaintainer, fleet.RoleObserverPlus:
return defaultAllowClause
case fleet.RoleObserver:
if filter.IncludeObserver {
return defaultAllowClause
}
return "FALSE"
default:
// Fall through to specific teams
}
@ -877,7 +875,9 @@ func (ds *Datastore) whereFilterHostsByTeams(filter fleet.TeamFilter, hostKey st
var idStrs []string
var teamIDSeen bool
for _, team := range filter.User.Teams {
if team.Role == fleet.RoleAdmin || team.Role == fleet.RoleMaintainer ||
if team.Role == fleet.RoleAdmin ||
team.Role == fleet.RoleMaintainer ||
team.Role == fleet.RoleObserverPlus ||
(team.Role == fleet.RoleObserver && filter.IncludeObserver) {
idStrs = append(idStrs, strconv.Itoa(int(team.ID)))
if filter.TeamID != nil && *filter.TeamID == team.ID {
@ -918,16 +918,13 @@ func (ds *Datastore) whereFilterTeams(filter fleet.TeamFilter, teamKey string) s
if filter.User.GlobalRole != nil {
switch *filter.User.GlobalRole {
case fleet.RoleAdmin, fleet.RoleMaintainer:
case fleet.RoleAdmin, fleet.RoleMaintainer, fleet.RoleObserverPlus:
return "TRUE"
case fleet.RoleObserver:
if filter.IncludeObserver {
return "TRUE"
}
return "FALSE"
default:
// Fall through to specific teams
}
@ -936,7 +933,9 @@ func (ds *Datastore) whereFilterTeams(filter fleet.TeamFilter, teamKey string) s
// Collect matching teams
var idStrs []string
for _, team := range filter.User.Teams {
if team.Role == fleet.RoleAdmin || team.Role == fleet.RoleMaintainer ||
if team.Role == fleet.RoleAdmin ||
team.Role == fleet.RoleMaintainer ||
team.Role == fleet.RoleObserverPlus ||
(team.Role == fleet.RoleObserver && filter.IncludeObserver) {
idStrs = append(idStrs, strconv.Itoa(int(team.ID)))
}

View File

@ -167,7 +167,10 @@ func parseRole(values []SAMLAttributeValue) (string, error) {
}
// Using last value by default.
value := values[len(values)-1].Value
if value != RoleAdmin && value != RoleMaintainer && value != RoleObserver {
if value != RoleAdmin &&
value != RoleMaintainer &&
value != RoleObserver &&
value != RoleObserverPlus {
return "", fmt.Errorf("invalid role: %s", value)
}
return value, nil

View File

@ -11,6 +11,7 @@ const (
RoleAdmin = "admin"
RoleMaintainer = "maintainer"
RoleObserver = "observer"
RoleObserverPlus = "observer_plus"
)
type TeamPayload struct {
@ -195,15 +196,21 @@ type TeamUser struct {
Role string `json:"role" db:"role"`
}
var teamRoles = map[string]bool{
RoleAdmin: true,
RoleObserver: true,
RoleMaintainer: true,
var teamRoles = map[string]struct{}{
RoleAdmin: {},
RoleObserver: {},
RoleMaintainer: {},
RoleObserverPlus: {},
}
var premiumTeamRoles = map[string]struct{}{
RoleObserverPlus: {},
}
// ValidTeamRole returns whether the role provided is valid for a team user.
func ValidTeamRole(role string) bool {
return teamRoles[role]
_, ok := teamRoles[role]
return ok
}
// ValidTeamRoles returns the list of valid roles for a team user.
@ -215,15 +222,21 @@ func ValidTeamRoles() []string {
return roles
}
var globalRoles = map[string]bool{
RoleObserver: true,
RoleMaintainer: true,
RoleAdmin: true,
var globalRoles = map[string]struct{}{
RoleObserver: {},
RoleMaintainer: {},
RoleAdmin: {},
RoleObserverPlus: {},
}
var premiumGlobalRoles = map[string]struct{}{
RoleObserverPlus: {},
}
// ValidGlobalRole returns whether the role provided is valid for a global user.
func ValidGlobalRole(role string) bool {
return globalRoles[role]
_, ok := globalRoles[role]
return ok
}
// ValidGlobalRoles returns the list of valid roles for a global user.
@ -244,7 +257,7 @@ func ValidateRole(globalRole *string, teamUsers []UserTeam) error {
}
for _, t := range teamUsers {
if !ValidTeamRole(t.Role) {
return NewError(ErrNoRoleNeeded, "Team roles can be observer or maintainer")
return NewErrorf(ErrNoRoleNeeded, "invalid team role: %s", t.Role)
}
}
return nil
@ -255,12 +268,37 @@ func ValidateRole(globalRole *string, teamUsers []UserTeam) error {
}
if !ValidGlobalRole(*globalRole) {
return NewError(ErrNoRoleNeeded, "GlobalRole role can only be admin, observer, or maintainer.")
return NewErrorf(ErrNoRoleNeeded, "invalid global role: %s", *globalRole)
}
return nil
}
func ValidateRoleForLicense(globalRole *string, teamUsers *[]UserTeam, license LicenseInfo) error {
var teamUsers_ []UserTeam
if teamUsers != nil {
teamUsers_ = *teamUsers
}
if err := ValidateRole(globalRole, teamUsers_); err != nil {
return err
}
premiumRolesPresent := false
if globalRole != nil {
if _, ok := premiumGlobalRoles[*globalRole]; ok {
premiumRolesPresent = true
}
}
for _, teamUser := range teamUsers_ {
if _, ok := premiumTeamRoles[teamUser.Role]; ok {
premiumRolesPresent = true
}
}
if !license.IsPremium() && premiumRolesPresent {
return ErrMissingLicense
}
return nil
}
// TeamFilter is the filtering information passed to the datastore for queries
// that may be filtered by team.
type TeamFilter struct {

View File

@ -16,7 +16,7 @@ func TestListActivities(t *testing.T) {
ds := new(mock.Store)
svc, ctx := newTestService(t, ds, nil, nil)
globalUsers := []*fleet.User{test.UserAdmin, test.UserMaintainer, test.UserObserver}
globalUsers := []*fleet.User{test.UserAdmin, test.UserMaintainer, test.UserObserver, test.UserObserverPlus}
teamUsers := []*fleet.User{test.UserTeamAdminTeam1, test.UserTeamMaintainerTeam1, test.UserTeamObserverTeam1}
ds.ListActivitiesFunc = func(ctx context.Context, opts fleet.ListActivitiesOptions) ([]*fleet.Activity, *fleet.PaginationMetadata, error) {

View File

@ -203,6 +203,7 @@ func TestAppleMDMAuthorization(t *testing.T) {
test.UserNoRoles,
test.UserMaintainer,
test.UserObserver,
test.UserObserverPlus,
test.UserTeamAdminTeam1,
} {
testAuthdMethods(t, user, true)
@ -339,6 +340,10 @@ func TestAppleMDMAuthorization(t *testing.T) {
{"observer can view", test.UserObserver, "uuidTm2", false},
{"observer can view", test.UserObserver, "uuidNoTm", false},
{"observer can view", test.UserObserver, "uuidMixTm1Tm2", false},
{"observer+ can view", test.UserObserverPlus, "uuidTm1", false},
{"observer+ can view", test.UserObserverPlus, "uuidTm2", false},
{"observer+ can view", test.UserObserverPlus, "uuidNoTm", false},
{"observer+ can view", test.UserObserverPlus, "uuidMixTm1Tm2", false},
{"admin can view", test.UserAdmin, "uuidTm1", false},
{"admin can view", test.UserAdmin, "uuidTm2", false},
{"admin can view", test.UserAdmin, "uuidNoTm", false},
@ -351,6 +356,10 @@ func TestAppleMDMAuthorization(t *testing.T) {
{"tm1 observer cannot view tm2", test.UserTeamObserverTeam1, "uuidTm2", true},
{"tm1 observer cannot view no team", test.UserTeamObserverTeam1, "uuidNoTm", true},
{"tm1 observer cannot view mix", test.UserTeamObserverTeam1, "uuidMixTm1Tm2", true},
{"tm1 observer+ can view tm1", test.UserTeamObserverPlusTeam1, "uuidTm1", false},
{"tm1 observer+ cannot view tm2", test.UserTeamObserverPlusTeam1, "uuidTm2", true},
{"tm1 observer+ cannot view no team", test.UserTeamObserverPlusTeam1, "uuidNoTm", true},
{"tm1 observer+ cannot view mix", test.UserTeamObserverPlusTeam1, "uuidMixTm1Tm2", true},
{"tm1 admin can view tm1", test.UserTeamAdminTeam1, "uuidTm1", false},
{"tm1 admin cannot view tm2", test.UserTeamAdminTeam1, "uuidTm2", true},
{"tm1 admin cannot view no team", test.UserTeamAdminTeam1, "uuidNoTm", true},
@ -774,6 +783,7 @@ func TestAppleMDMEnrollmentProfile(t *testing.T) {
test.UserNoRoles,
test.UserMaintainer,
test.UserObserver,
test.UserObserverPlus,
test.UserTeamAdminTeam1,
} {
ctx := test.UserContext(ctx, user)

View File

@ -689,6 +689,7 @@ func TestRefetchHost(t *testing.T) {
require.NoError(t, svc.RefetchHost(test.UserContext(ctx, test.UserAdmin), host.ID))
require.NoError(t, svc.RefetchHost(test.UserContext(ctx, test.UserObserver), host.ID))
require.NoError(t, svc.RefetchHost(test.UserContext(ctx, test.UserObserverPlus), host.ID))
require.NoError(t, svc.RefetchHost(test.UserContext(ctx, test.UserMaintainer), host.ID))
assert.True(t, ds.HostLiteFuncInvoked)
assert.True(t, ds.UpdateHostRefetchRequestedFuncInvoked)
@ -809,6 +810,7 @@ func TestHostEncryptionKey(t *testing.T) {
test.UserAdmin,
test.UserMaintainer,
test.UserObserver,
test.UserObserverPlus,
},
disallowedUsers: []*fleet.User{
test.UserTeamAdminTeam1,
@ -831,14 +833,17 @@ func TestHostEncryptionKey(t *testing.T) {
test.UserAdmin,
test.UserMaintainer,
test.UserObserver,
test.UserObserverPlus,
test.UserTeamAdminTeam1,
test.UserTeamMaintainerTeam1,
test.UserTeamObserverTeam1,
test.UserTeamObserverPlusTeam1,
},
disallowedUsers: []*fleet.User{
test.UserTeamAdminTeam2,
test.UserTeamMaintainerTeam2,
test.UserTeamObserverTeam2,
test.UserTeamObserverPlusTeam2,
test.UserNoRoles,
},
},

View File

@ -204,7 +204,7 @@ func (s *integrationTestSuite) TestUserWithWrongRoleErrors() {
GlobalRole: ptr.String("wrongrole"),
}
resp := s.Do("POST", "/api/latest/fleet/users/admin", &params, http.StatusUnprocessableEntity)
assertErrorCodeAndMessage(t, resp, fleet.ErrNoRoleNeeded, "GlobalRole role can only be admin, observer, or maintainer.")
assertErrorCodeAndMessage(t, resp, fleet.ErrNoRoleNeeded, "invalid global role: wrongrole")
}
func (s *integrationTestSuite) TestUserCreationWrongTeamErrors() {

View File

@ -8,6 +8,7 @@ import (
"io"
"net/http"
"net/http/httptest"
"os"
"reflect"
"sort"
"strings"
@ -20,6 +21,7 @@ import (
"github.com/fleetdm/fleet/v4/server/live_query/live_query_mock"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/go-kit/log"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
@ -53,6 +55,7 @@ func (s *integrationEnterpriseTestSuite) SetupSuite() {
},
Pool: s.redisPool,
Lq: s.lq,
Logger: log.NewLogfmtLogger(os.Stdout),
}
users, server := RunServerForTestsWithDS(s.T(), s.ds, &config)
s.server = server

View File

@ -79,6 +79,7 @@ func TestMDMAppleAuthorization(t *testing.T) {
test.UserNoRoles,
test.UserMaintainer,
test.UserObserver,
test.UserObserverPlus,
test.UserTeamAdminTeam1,
} {
testAuthdMethods(t, user, true)

View File

@ -5,6 +5,7 @@ import (
"github.com/fleetdm/fleet/v4/server/authz"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/contexts/license"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
)
@ -30,6 +31,14 @@ func (svc *Service) CreateInitialUser(ctx context.Context, p fleet.UserPayload)
}
func (svc *Service) NewUser(ctx context.Context, p fleet.UserPayload) (*fleet.User, error) {
license, _ := license.FromContext(ctx)
if license == nil {
return nil, ctxerr.New(ctx, "license not found")
}
if err := fleet.ValidateRoleForLicense(p.GlobalRole, p.Teams, *license); err != nil {
return nil, ctxerr.Wrap(ctx, err, "validate role")
}
user, err := p.User(svc.config.Auth.SaltKeySize, svc.config.Auth.BcryptCost)
if err != nil {
return nil, err

View File

@ -13,6 +13,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/fleetdm/fleet/v4/server/mail"
@ -297,6 +298,13 @@ func (svc *Service) ModifyUser(ctx context.Context, userID uint, p fleet.UserPay
if err := svc.authz.Authorize(ctx, user, fleet.ActionWriteRole); err != nil {
return nil, err
}
license, _ := license.FromContext(ctx)
if license == nil {
return nil, ctxerr.New(ctx, "license not found")
}
if err := fleet.ValidateRoleForLicense(p.GlobalRole, p.Teams, *license); err != nil {
return nil, ctxerr.Wrap(ctx, err, "validate role")
}
}
if p.NewPassword != nil {

View File

@ -91,4 +91,26 @@ var (
},
},
}
UserObserverPlus = &fleet.User{
ID: 12,
GlobalRole: ptr.String(fleet.RoleObserverPlus),
}
UserTeamObserverPlusTeam1 = &fleet.User{
ID: 13,
Teams: []fleet.UserTeam{
{
Team: fleet.Team{ID: 1},
Role: fleet.RoleObserverPlus,
},
},
}
UserTeamObserverPlusTeam2 = &fleet.User{
ID: 14,
Teams: []fleet.UserTeam{
{
Team: fleet.Team{ID: 2},
Role: fleet.RoleObserverPlus,
},
},
}
)