diff --git a/changes/8593-observer_plus b/changes/8593-observer_plus new file mode 100644 index 000000000..2e5e237d7 --- /dev/null +++ b/changes/8593-observer_plus @@ -0,0 +1 @@ +* Add `observer_plus` user role to Fleet. Observers+ are observers that can run any live query. diff --git a/docs/Deploying/Configuration.md b/docs/Deploying/Configuration.md index e45464482..65880bb6e 100644 --- a/docs/Deploying/Configuration.md +++ b/docs/Deploying/Configuration.md @@ -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_`: Specifies team role for team with 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_` will cause an error during login as Fleet users cannot be Global users and belong to teams. diff --git a/docs/Using-Fleet/Permissions.md b/docs/Using-Fleet/Permissions.md index bd067b7d1..40dc3fdcf 100644 --- a/docs/Using-Fleet/Permissions.md +++ b/docs/Using-Fleet/Permissions.md @@ -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) diff --git a/docs/Using-Fleet/REST-API.md b/docs/Using-Fleet/REST-API.md index 2f3c8ea73..b54e45832 100644 --- a/docs/Using-Fleet/REST-API.md +++ b/docs/Using-Fleet/REST-API.md @@ -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 diff --git a/server/authz/policy.rego b/server/authz/policy.rego index 3d24af502..a7ad8db4b 100644 --- a/server/authz/policy.rego +++ b/server/authz/policy.rego @@ -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 } diff --git a/server/authz/policy_test.go b/server/authz/policy_test.go index 33d6b9f31..aa8318c12 100644 --- a/server/authz/policy_test.go +++ b/server/authz/policy_test.go @@ -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}, }) } diff --git a/server/datastore/mysql/mysql.go b/server/datastore/mysql/mysql.go index d353cfa7b..98dd5fce3 100644 --- a/server/datastore/mysql/mysql.go +++ b/server/datastore/mysql/mysql.go @@ -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))) } diff --git a/server/fleet/sessions.go b/server/fleet/sessions.go index 64419c78c..04693aed8 100644 --- a/server/fleet/sessions.go +++ b/server/fleet/sessions.go @@ -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 diff --git a/server/fleet/teams.go b/server/fleet/teams.go index 85293c9d3..e0624bbe5 100644 --- a/server/fleet/teams.go +++ b/server/fleet/teams.go @@ -8,9 +8,10 @@ import ( ) const ( - RoleAdmin = "admin" - RoleMaintainer = "maintainer" - RoleObserver = "observer" + 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 { diff --git a/server/service/activities_test.go b/server/service/activities_test.go index 3362a4371..6240d81f1 100644 --- a/server/service/activities_test.go +++ b/server/service/activities_test.go @@ -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) { diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index 018259365..284a7bcba 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -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) diff --git a/server/service/hosts_test.go b/server/service/hosts_test.go index ddaa5dbf2..2b056c756 100644 --- a/server/service/hosts_test.go +++ b/server/service/hosts_test.go @@ -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, }, }, diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index f0ce568db..877c0db06 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -204,7 +204,7 @@ func (s *integrationTestSuite) TestUserWithWrongRoleErrors() { GlobalRole: ptr.String("wrongrole"), } resp := s.Do("POST", "/api/latest/fleet/users/admin", ¶ms, 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() { diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index bba1677fe..c3277d21d 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -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" @@ -51,8 +53,9 @@ func (s *integrationEnterpriseTestSuite) SetupSuite() { License: &fleet.LicenseInfo{ Tier: fleet.TierPremium, }, - Pool: s.redisPool, - Lq: s.lq, + Pool: s.redisPool, + Lq: s.lq, + Logger: log.NewLogfmtLogger(os.Stdout), } users, server := RunServerForTestsWithDS(s.T(), s.ds, &config) s.server = server diff --git a/server/service/mdm_test.go b/server/service/mdm_test.go index f29fc14e5..1b155452b 100644 --- a/server/service/mdm_test.go +++ b/server/service/mdm_test.go @@ -79,6 +79,7 @@ func TestMDMAppleAuthorization(t *testing.T) { test.UserNoRoles, test.UserMaintainer, test.UserObserver, + test.UserObserverPlus, test.UserTeamAdminTeam1, } { testAuthdMethods(t, user, true) diff --git a/server/service/service_users.go b/server/service/service_users.go index 217f41928..0d4c229a8 100644 --- a/server/service/service_users.go +++ b/server/service/service_users.go @@ -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 diff --git a/server/service/users.go b/server/service/users.go index f7e43bc0a..5bd64f044 100644 --- a/server/service/users.go +++ b/server/service/users.go @@ -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 { diff --git a/server/test/users.go b/server/test/users.go index 15748490b..a9aed869e 100644 --- a/server/test/users.go +++ b/server/test/users.go @@ -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, + }, + }, + } )