diff --git a/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md b/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md index 0754a207309..cfe97214b54 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md @@ -47,6 +47,7 @@ The following list contains role-based access control actions. | `alert.rules:read` | `folders:*`
`folders:uid:*` | Read Grafana alert rules in a folder and its subfolders. Combine this permission with `folders:read` in a scope that includes the folder and `datasources:query` in the scope of data sources the user can query. | | `alert.rules:write` | `folders:*`
`folders:uid:*` | Update Grafana alert rules in a folder and its subfolders. Combine this permission with `folders:read` in a scope that includes the folder and `datasources:query` in the scope of data sources the user can query. | | `alert.provisioning:read` | n/a | Read all Grafana alert rules, notification policies, etc via provisioning API. Permissions to folders and datasource are not required. | +| `alert.provisioning.secrets:read` | n/a | Same as `alert.provisioning:read` plus ability to export resources with decrypted secrets. | | `alert.provisioning:write` | n/a | Update all Grafana alert rules, notification policies, etc via provisioning API. Permissions to folders and datasource are not required. | | `annotations:create` | `annotations:*`
`annotations:type:*` | Create annotations. | | `annotations:delete` | `annotations:*`
`annotations:type:*` | Delete annotations. | diff --git a/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md b/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md index fc24c05c13e..6bc67b36c4d 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md @@ -23,81 +23,82 @@ The following tables list permissions associated with basic and fixed roles. ## Basic role assignments -| Basic role | Associated fixed roles | Description | -| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -| Grafana Admin | `fixed:roles:reader`
`fixed:roles:writer`
`fixed:users:reader`
`fixed:users:writer`
`fixed:org.users:reader`
`fixed:org.users:writer`
`fixed:ldap:reader`
`fixed:ldap:writer`
`fixed:stats:reader`
`fixed:settings:reader`
`fixed:settings:writer`
`fixed:provisioning:writer`
`fixed:organization:reader`
`fixed:organization:maintainer`
`fixed:licensing:reader`
`fixed:licensing:writer`
`fixed:datasources.caching:reader`
`fixed:datasources.caching:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader`
`fixed:plugins:maintainer`
`fixed:authentication.config:writer` | Default [Grafana server administrator]({{< relref "../#grafana-server-administrators" >}}) assignments. | -| Admin | `fixed:reports:reader`
`fixed:reports:writer`
`fixed:datasources:reader`
`fixed:datasources:writer`
`fixed:organization:writer`
`fixed:datasources.permissions:reader`
`fixed:datasources.permissions:writer`
`fixed:teams:writer`
`fixed:dashboards:reader`
`fixed:dashboards:writer`
`fixed:dashboards.permissions:reader`
`fixed:dashboards.permissions:writer`
`fixed:dashboards.public:writer`
`fixed:folders:reader`
`fixed:folders:writer`
`fixed:folders.permissions:reader`
`fixed:folders.permissions:writer`
`fixed:alerting:writer`
`fixed:apikeys:reader`
`fixed:apikeys:writer`
`fixed:alerting.provisioning:writer`
`fixed:datasources.caching:reader`
`fixed:datasources.caching:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader`
`fixed:plugins:writer` | Default [Grafana organization administrator]({{< relref "../#organization-users-and-permissions" >}}) assignments. | -| Editor | `fixed:datasources:explorer`
`fixed:dashboards:creator`
`fixed:folders:creator`
`fixed:annotations:writer`
`fixed:teams:creator` if the `editors_can_admin` configuration flag is enabled
`fixed:alerting:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader` | Default [Editor]({{< relref "../#organization-users-and-permissions" >}}) assignments. | -| Viewer | `fixed:datasources:id:reader`
`fixed:organization:reader`
`fixed:annotations:reader`
`fixed:annotations.dashboard:writer`
`fixed:alerting:reader`
`fixed:plugins.app:reader`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader` | Default [Viewer]({{< relref "../#organization-users-and-permissions" >}}) assignments. | +| Basic role | Associated fixed roles | Description | +| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| Grafana Admin | `fixed:roles:reader`
`fixed:roles:writer`
`fixed:users:reader`
`fixed:users:writer`
`fixed:org.users:reader`
`fixed:org.users:writer`
`fixed:ldap:reader`
`fixed:ldap:writer`
`fixed:stats:reader`
`fixed:settings:reader`
`fixed:settings:writer`
`fixed:provisioning:writer`
`fixed:organization:reader`
`fixed:organization:maintainer`
`fixed:licensing:reader`
`fixed:licensing:writer`
`fixed:datasources.caching:reader`
`fixed:datasources.caching:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader`
`fixed:plugins:maintainer`
`fixed:authentication.config:writer` | Default [Grafana server administrator]({{< relref "../#grafana-server-administrators" >}}) assignments. | +| Admin | `fixed:reports:reader`
`fixed:reports:writer`
`fixed:datasources:reader`
`fixed:datasources:writer`
`fixed:organization:writer`
`fixed:datasources.permissions:reader`
`fixed:datasources.permissions:writer`
`fixed:teams:writer`
`fixed:dashboards:reader`
`fixed:dashboards:writer`
`fixed:dashboards.permissions:reader`
`fixed:dashboards.permissions:writer`
`fixed:dashboards.public:writer`
`fixed:folders:reader`
`fixed:folders:writer`
`fixed:folders.permissions:reader`
`fixed:folders.permissions:writer`
`fixed:alerting:writer`
`fixed:apikeys:reader`
`fixed:apikeys:writer`
`fixed:alerting.provisioning.secrets:reader`
`fixed:alerting.provisioning:writer`
`fixed:datasources.caching:reader`
`fixed:datasources.caching:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader`
`fixed:plugins:writer` | Default [Grafana organization administrator]({{< relref "../#organization-users-and-permissions" >}}) assignments. | +| Editor | `fixed:datasources:explorer`
`fixed:dashboards:creator`
`fixed:folders:creator`
`fixed:annotations:writer`
`fixed:teams:creator` if the `editors_can_admin` configuration flag is enabled
`fixed:alerting:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader` | Default [Editor]({{< relref "../#organization-users-and-permissions" >}}) assignments. | +| Viewer | `fixed:datasources:id:reader`
`fixed:organization:reader`
`fixed:annotations:reader`
`fixed:annotations.dashboard:writer`
`fixed:alerting:reader`
`fixed:plugins.app:reader`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader` | Default [Viewer]({{< relref "../#organization-users-and-permissions" >}}) assignments. | ## Fixed role definitions -| Fixed role | Permissions | Description | -| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `fixed:alerting.instances:writer` | All permissions from `fixed:alerting.instances:reader` and
`alert.instances:create`
`alert.instances:write` for organization scope
`alert.instances.external:write` for scope `datasources:*` | Create, update and expire all silences in the organization produced by Grafana, Mimir, and Loki.[\*](#alerting-roles) | -| `fixed:alerting.instances:reader` | `alert.instances:read` for organization scope
`alert.instances.external:read` for scope `datasources:*` | Read all alerts and silences in the organization produced by Grafana Alerts and Mimir and Loki alerts and silences.[\*](#alerting-roles) | -| `fixed:alerting.notifications:writer` | All permissions from `fixed:alerting.notifications:reader` and
`alert.notifications:write`for organization scope
`alert.notifications.external:read` for scope `datasources:*` | Create, update, and delete contact points, templates, mute timings and notification policies for Grafana and external Alertmanager.[\*](#alerting-roles) | -| `fixed:alerting.notifications:reader` | `alert.notifications:read` for organization scope
`alert.notifications.external:read` for scope `datasources:*` | Read all Grafana and Alertmanager contact points, templates, and notification policies.[\*](#alerting-roles) | -| `fixed:alerting.rules:writer` | All permissions from `fixed:alerting.rules:reader` and
`alert.rule:create`
`alert.rule:write`
`alert.rule:delete` for scope `folders:*`
`alert.rules.external:write` for scope `datasources:*` | Create, update, and delete all\* Grafana, Mimir, and Loki alert rules.[\*](#alerting-roles) | -| `fixed:alerting.rules:reader` | `alert.rule:read` for scope `folders:*`
`alert.rules.external:read` for scope `datasources:*` | Read all\* Grafana, Mimir, and Loki alert rules.[\*](#alerting-roles) | -| `fixed:alerting:writer` | All permissions from `fixed:alerting.rules:writer`
`fixed:alerting.instances:writer`
`fixed:alerting.notifications:writer` | Create, update, and delete Grafana, Mimir, Loki and Alertmanager alert rules\*, silences, contact points, templates, mute timings, and notification policies.[\*](#alerting-roles) | -| `fixed:alerting:reader` | All permissions from `fixed:alerting.rules:reader`
`fixed:alerting.instances:reader`
`fixed:alerting.notifications:reader` | Read-only permissions for all Grafana, Mimir, Loki and Alertmanager alert rules\*, alerts, contact points, and notification policies.[\*](#alerting-roles) | -| `fixed:alerting.provisioning:writer` | `alert.provisioning:read` and `alert.provisioning:write` | Create, update and delete Grafana alert rules, notification policies, contact points, templates, etc via provisioning API. [\*](#alerting-roles) | -| `fixed:annotations.dashboard:writer` | `annotations:write`
`annotations.create`
`annotations:delete` for scope `annotations:type:dashboard` | Create, update and delete dashboard annotations and annotation tags. | -| `fixed:annotations:reader` | `annotations:read` for scopes `annotations:type:*` | Read all annotations and annotation tags. | -| `fixed:annotations:writer` | All permissions from `fixed:annotations:reader`
`annotations:write`
`annotations.create`
`annotations:delete` for scope `annotations:type:*` | Read, create, update and delete all annotations and annotation tags. | -| `fixed:apikeys:reader` | `apikeys:read` for scope `apikeys:*` | Read all api keys. | -| `fixed:apikeys:writer` | All permissions from `fixed:apikeys:reader` and
`apikeys:create`
`apikeys:delete` for scope `apikeys:*` | Read, create, delete all api keys. | -| `fixed:authentication.config:writer` | `settings:read` for scope `settings:auth.saml:*`
`settings:write` for scope `settings:auth.saml:*` | Read and update authentication and SAML settings. | -| `fixed:dashboards:creator` | `dashboards:create`
`folders:read` | Create dashboards. | -| `fixed:dashboards.insights:reader` | `dashboards.insights:read` | Read dashboard insights data and see presence indicators. | -| `fixed:dashboards.permissions:reader` | `dashboards.permissions:read` | Read all dashboard permissions. | -| `fixed:dashboards.permissions:writer` | All permissions from `fixed:dashboards.permissions:reader` and
`dashboards.permissions:write` | Read and update all dashboard permissions. | -| `fixed:dashboards.public:writer` | `dashboards.public:write` | Create, update, delete or pause a public dashboard. | -| `fixed:dashboards:reader` | `dashboards:read` | Read all dashboards. | -| `fixed:dashboards:writer` | All permissions from `fixed:dashboards:reader` and
`dashboards:write`
`dashboards:edit`
`dashboards:delete`
`dashboards:create`
`dashboards.permissions:read`
`dashboards.permissions:write` | Read, create, update, and delete all dashboards. | -| `fixed:datasources.caching:reader` | `datasources.caching:read` | Read data source query caching settings. | -| `fixed:datasources.caching:writer` | `datasources.caching:read`
`datasources.caching:write` | Enable, disable, or update query caching settings. | -| `fixed:datasources:explorer` | `datasources:explore` | Enable the Explore feature. Data source permissions still apply, you can only query data sources for which you have query permissions. | -| `fixed:datasources:id:reader` | `datasources.id:read` | Read the ID of a data source based on its name. | -| `fixed:datasources.insights:reader` | `datasources.insights:read` | Read data source insights data. | -| `fixed:datasources.permissions:reader` | `datasources.permissions:read` | Read data source permissions. | -| `fixed:datasources.permissions:writer` | All permissions from `fixed:datasources.permissions:reader` and
`datasources.permissions:write` | Create, read, or delete permissions of a data source. | -| `fixed:datasources:reader` | `datasources:read`
`datasources:query` | Read and query data sources. | -| `fixed:datasources:writer` | All permissions from `fixed:datasources:reader` and
`datasources:create`
`datasources:write`
`datasources:delete` | Read, query, create, delete, or update a data source. | -| `fixed:folders.permissions:reader` | `folders.permissions:read` | Read all folder permissions. | -| `fixed:folders.permissions:writer` | All permissions from `fixed:folders.permissions:reader` and
`folders.permissions:write` | Read and update all folder permissions. | -| `fixed:folders:creator` | `folders:create` | Create folders in the root level. If granted together with `folders:write` permission, also allows creating subfolders under all folders. | -| `fixed:folders:reader` | `folders:read`
`dashboards:read` | Read all folders and dashboards. | -| `fixed:folders:writer` | All permissions from `fixed:dashboards:writer` and
`folders:read`
`folders:write`
`folders:create`
`folders:delete`
`folders.permissions:read`
`folders.permissions:write` | Read, create, update, and delete all folders and dashboards. If granted together with `fixed:folders:creator`, allows creating subfolders under all folders. | -| `fixed:ldap:reader` | `ldap.user:read`
`ldap.status:read` | Read the LDAP configuration and LDAP status information. | -| `fixed:ldap:writer` | All permissions from `fixed:ldap:reader` and
`ldap.user:sync`
`ldap.config:reload` | Read and update the LDAP configuration, and read LDAP status information. | -| `fixed:licensing:reader` | `licensing:read`
`licensing.reports:read` | Read licensing information and licensing reports. | -| `fixed:licensing:writer` | All permissions from `fixed:licensing:viewer` and
`licensing:write`
`licensing:delete` | Read licensing information and licensing reports, update and delete the license token. | -| `fixed:org.users:reader` | `org.users:read` | Read users within a single organization. | -| `fixed:org.users:writer` | All permissions from `fixed:org.users:reader` and
`org.users:add`
`org.users:remove`
`org.users:write` | Within a single organization, add a user, invite a new user, read information about a user and their role, remove a user from that organization, or change the role of a user. | -| `fixed:organization:maintainer` | All permissions from `fixed:organization:reader` and
`orgs:write`
`orgs:create`
`orgs:delete`
`orgs.quotas:write` | Create, read, write, or delete an organization. Read or write its quotas. This role needs to be assigned globally. | -| `fixed:organization:reader` | `orgs:read`
`orgs.quotas:read` | Read an organization and its quotas. | -| `fixed:organization:writer` | All permissions from `fixed:organization:reader` and
`orgs:write`
`orgs.preferences:read`
`orgs.preferences:write` | Read an organization, its quotas, or its preferences. Update organization properties, or its preferences. | -| `fixed:plugins.app:reader` | `plugins.app:access` | Access application plugins (still enforcing the organization role). | -| `fixed:plugins:maintainer` | `plugins:install` | Install and uninstall plugins. | -| `fixed:plugins:writer` | `plugins:write` | Enable and disable plugins and edit plugins' settings. | -| `fixed:provisioning:writer` | `provisioning:reload` | Reload provisioning. | -| `fixed:reports:reader` | `reports:read`
`reports:send`
`reports.settings:read` | Read all reports and shared report settings. | -| `fixed:reports:writer` | All permissions from `fixed:reports:reader` and
`reports:create`
`reports:write`
`reports:delete`
`reports.settings:write` | Create, read, update, or delete all reports and shared report settings. | -| `fixed:roles:reader` | `roles:read`
`teams.roles:read`
`users.roles:read`
`users.permissions:read` | Read all access control roles, roles and permissions assigned to users, teams. | -| `fixed:roles:writer` | All permissions from `fixed:roles:reader` and
`roles:write`
`roles:delete`
`teams.roles:add`
`teams.roles:remove`
`users.roles:add`
`users.roles:remove` | Create, read, update, or delete all roles, assign or unassign roles to users, teams. | -| `fixed:roles:resetter` | `roles:write` with scope `permissions:type:escalate` | Reset basic roles to their default. | -| `fixed:serviceaccounts:reader` | `serviceaccounts:read` | Read Grafana service accounts. | -| `fixed:serviceaccounts:creator` | `serviceaccounts:create` | Create Grafana service accounts. | -| `fixed:serviceaccounts:writer` | `serviceaccounts:read`
`serviceaccounts:create`
`serviceaccounts:write`
`serviceaccounts:delete`
`serviceaccounts.permissions:read`
`serviceaccounts.permissions:write` | Create, update, read and delete all Grafana service accounts and manage service account permissions. | -| `fixed:settings:reader` | `settings:read` | Read Grafana instance settings. | -| `fixed:settings:writer` | All permissions from `fixed:settings:reader` and
`settings:write` | Read and update Grafana instance settings. | -| `fixed:stats:reader` | `server.stats:read` | Read Grafana instance statistics. | -| `fixed:teams:creator` | `teams:create`
`org.users:read` | Create a team and list organization users (required to manage the created team). | -| `fixed:teams:writer` | `teams:create`
`teams:delete`
`teams:read`
`teams:write`
`teams.permissions:read`
`teams.permissions:write` | Create, read, update and delete teams and manage team memberships. | -| `fixed:users:reader` | `users:read`
`users.quotas:read`
`users.authtoken:read`
` | Read all users and their information, such as team memberships, authentication tokens, and quotas. | -| `fixed:users:writer` | All permissions from `fixed:users:reader` and
`users:write`
`users:create`
`users:delete`
`users:enable`
`users:disable`
`users.password:write`
`users.permissions:write`
`users:logout`
`users.authtoken:write`
`users.quotas:write` | Read and update all attributes and settings for all users in Grafana: update user information, read user information, create or enable or disable a user, make a user a Grafana administrator, sign out a user, update a user’s authentication token, or update quotas for all users. | +| Fixed role | Permissions | Description | +| -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `fixed:alerting.instances:writer` | All permissions from `fixed:alerting.instances:reader` and
`alert.instances:create`
`alert.instances:write` for organization scope
`alert.instances.external:write` for scope `datasources:*` | Create, update and expire all silences in the organization produced by Grafana, Mimir, and Loki.[\*](#alerting-roles) | +| `fixed:alerting.instances:reader` | `alert.instances:read` for organization scope
`alert.instances.external:read` for scope `datasources:*` | Read all alerts and silences in the organization produced by Grafana Alerts and Mimir and Loki alerts and silences.[\*](#alerting-roles) | +| `fixed:alerting.notifications:writer` | All permissions from `fixed:alerting.notifications:reader` and
`alert.notifications:write`for organization scope
`alert.notifications.external:read` for scope `datasources:*` | Create, update, and delete contact points, templates, mute timings and notification policies for Grafana and external Alertmanager.[\*](#alerting-roles) | +| `fixed:alerting.notifications:reader` | `alert.notifications:read` for organization scope
`alert.notifications.external:read` for scope `datasources:*` | Read all Grafana and Alertmanager contact points, templates, and notification policies.[\*](#alerting-roles) | +| `fixed:alerting.rules:writer` | All permissions from `fixed:alerting.rules:reader` and
`alert.rule:create`
`alert.rule:write`
`alert.rule:delete` for scope `folders:*`
`alert.rules.external:write` for scope `datasources:*` | Create, update, and delete all\* Grafana, Mimir, and Loki alert rules.[\*](#alerting-roles) | +| `fixed:alerting.rules:reader` | `alert.rule:read` for scope `folders:*`
`alert.rules.external:read` for scope `datasources:*` | Read all\* Grafana, Mimir, and Loki alert rules.[\*](#alerting-roles) | +| `fixed:alerting:writer` | All permissions from `fixed:alerting.rules:writer`
`fixed:alerting.instances:writer`
`fixed:alerting.notifications:writer` | Create, update, and delete Grafana, Mimir, Loki and Alertmanager alert rules\*, silences, contact points, templates, mute timings, and notification policies.[\*](#alerting-roles) | +| `fixed:alerting:reader` | All permissions from `fixed:alerting.rules:reader`
`fixed:alerting.instances:reader`
`fixed:alerting.notifications:reader` | Read-only permissions for all Grafana, Mimir, Loki and Alertmanager alert rules\*, alerts, contact points, and notification policies.[\*](#alerting-roles) | +| `fixed:alerting.provisioning.secrets:reader` | `alert.provisioning:read` and `alert.provisioning.secrets:read` | Read-only permissions for Provisioning API and let export resources with decrypted secrets [\*](#alerting-roles) | +| `fixed:alerting.provisioning:writer` | `alert.provisioning:read` and `alert.provisioning:write` | Create, update and delete Grafana alert rules, notification policies, contact points, templates, etc via provisioning API. [\*](#alerting-roles) | +| `fixed:annotations.dashboard:writer` | `annotations:write`
`annotations.create`
`annotations:delete` for scope `annotations:type:dashboard` | Create, update and delete dashboard annotations and annotation tags. | +| `fixed:annotations:reader` | `annotations:read` for scopes `annotations:type:*` | Read all annotations and annotation tags. | +| `fixed:annotations:writer` | All permissions from `fixed:annotations:reader`
`annotations:write`
`annotations.create`
`annotations:delete` for scope `annotations:type:*` | Read, create, update and delete all annotations and annotation tags. | +| `fixed:apikeys:reader` | `apikeys:read` for scope `apikeys:*` | Read all api keys. | +| `fixed:apikeys:writer` | All permissions from `fixed:apikeys:reader` and
`apikeys:create`
`apikeys:delete` for scope `apikeys:*` | Read, create, delete all api keys. | +| `fixed:authentication.config:writer` | `settings:read` for scope `settings:auth.saml:*`
`settings:write` for scope `settings:auth.saml:*` | Read and update authentication and SAML settings. | +| `fixed:dashboards:creator` | `dashboards:create`
`folders:read` | Create dashboards. | +| `fixed:dashboards.insights:reader` | `dashboards.insights:read` | Read dashboard insights data and see presence indicators. | +| `fixed:dashboards.permissions:reader` | `dashboards.permissions:read` | Read all dashboard permissions. | +| `fixed:dashboards.permissions:writer` | All permissions from `fixed:dashboards.permissions:reader` and
`dashboards.permissions:write` | Read and update all dashboard permissions. | +| `fixed:dashboards.public:writer` | `dashboards.public:write` | Create, update, delete or pause a public dashboard. | +| `fixed:dashboards:reader` | `dashboards:read` | Read all dashboards. | +| `fixed:dashboards:writer` | All permissions from `fixed:dashboards:reader` and
`dashboards:write`
`dashboards:edit`
`dashboards:delete`
`dashboards:create`
`dashboards.permissions:read`
`dashboards.permissions:write` | Read, create, update, and delete all dashboards. | +| `fixed:datasources.caching:reader` | `datasources.caching:read` | Read data source query caching settings. | +| `fixed:datasources.caching:writer` | `datasources.caching:read`
`datasources.caching:write` | Enable, disable, or update query caching settings. | +| `fixed:datasources:explorer` | `datasources:explore` | Enable the Explore feature. Data source permissions still apply, you can only query data sources for which you have query permissions. | +| `fixed:datasources:id:reader` | `datasources.id:read` | Read the ID of a data source based on its name. | +| `fixed:datasources.insights:reader` | `datasources.insights:read` | Read data source insights data. | +| `fixed:datasources.permissions:reader` | `datasources.permissions:read` | Read data source permissions. | +| `fixed:datasources.permissions:writer` | All permissions from `fixed:datasources.permissions:reader` and
`datasources.permissions:write` | Create, read, or delete permissions of a data source. | +| `fixed:datasources:reader` | `datasources:read`
`datasources:query` | Read and query data sources. | +| `fixed:datasources:writer` | All permissions from `fixed:datasources:reader` and
`datasources:create`
`datasources:write`
`datasources:delete` | Read, query, create, delete, or update a data source. | +| `fixed:folders.permissions:reader` | `folders.permissions:read` | Read all folder permissions. | +| `fixed:folders.permissions:writer` | All permissions from `fixed:folders.permissions:reader` and
`folders.permissions:write` | Read and update all folder permissions. | +| `fixed:folders:creator` | `folders:create` | Create folders in the root level. If granted together with `folders:write` permission, also allows creating subfolders under all folders. | +| `fixed:folders:reader` | `folders:read`
`dashboards:read` | Read all folders and dashboards. | +| `fixed:folders:writer` | All permissions from `fixed:dashboards:writer` and
`folders:read`
`folders:write`
`folders:create`
`folders:delete`
`folders.permissions:read`
`folders.permissions:write` | Read, create, update, and delete all folders and dashboards. If granted together with `fixed:folders:creator`, allows creating subfolders under all folders. | +| `fixed:ldap:reader` | `ldap.user:read`
`ldap.status:read` | Read the LDAP configuration and LDAP status information. | +| `fixed:ldap:writer` | All permissions from `fixed:ldap:reader` and
`ldap.user:sync`
`ldap.config:reload` | Read and update the LDAP configuration, and read LDAP status information. | +| `fixed:licensing:reader` | `licensing:read`
`licensing.reports:read` | Read licensing information and licensing reports. | +| `fixed:licensing:writer` | All permissions from `fixed:licensing:viewer` and
`licensing:write`
`licensing:delete` | Read licensing information and licensing reports, update and delete the license token. | +| `fixed:org.users:reader` | `org.users:read` | Read users within a single organization. | +| `fixed:org.users:writer` | All permissions from `fixed:org.users:reader` and
`org.users:add`
`org.users:remove`
`org.users:write` | Within a single organization, add a user, invite a new user, read information about a user and their role, remove a user from that organization, or change the role of a user. | +| `fixed:organization:maintainer` | All permissions from `fixed:organization:reader` and
`orgs:write`
`orgs:create`
`orgs:delete`
`orgs.quotas:write` | Create, read, write, or delete an organization. Read or write its quotas. This role needs to be assigned globally. | +| `fixed:organization:reader` | `orgs:read`
`orgs.quotas:read` | Read an organization and its quotas. | +| `fixed:organization:writer` | All permissions from `fixed:organization:reader` and
`orgs:write`
`orgs.preferences:read`
`orgs.preferences:write` | Read an organization, its quotas, or its preferences. Update organization properties, or its preferences. | +| `fixed:plugins.app:reader` | `plugins.app:access` | Access application plugins (still enforcing the organization role). | +| `fixed:plugins:maintainer` | `plugins:install` | Install and uninstall plugins. | +| `fixed:plugins:writer` | `plugins:write` | Enable and disable plugins and edit plugins' settings. | +| `fixed:provisioning:writer` | `provisioning:reload` | Reload provisioning. | +| `fixed:reports:reader` | `reports:read`
`reports:send`
`reports.settings:read` | Read all reports and shared report settings. | +| `fixed:reports:writer` | All permissions from `fixed:reports:reader` and
`reports:create`
`reports:write`
`reports:delete`
`reports.settings:write` | Create, read, update, or delete all reports and shared report settings. | +| `fixed:roles:reader` | `roles:read`
`teams.roles:read`
`users.roles:read`
`users.permissions:read` | Read all access control roles, roles and permissions assigned to users, teams. | +| `fixed:roles:writer` | All permissions from `fixed:roles:reader` and
`roles:write`
`roles:delete`
`teams.roles:add`
`teams.roles:remove`
`users.roles:add`
`users.roles:remove` | Create, read, update, or delete all roles, assign or unassign roles to users, teams. | +| `fixed:roles:resetter` | `roles:write` with scope `permissions:type:escalate` | Reset basic roles to their default. | +| `fixed:serviceaccounts:reader` | `serviceaccounts:read` | Read Grafana service accounts. | +| `fixed:serviceaccounts:creator` | `serviceaccounts:create` | Create Grafana service accounts. | +| `fixed:serviceaccounts:writer` | `serviceaccounts:read`
`serviceaccounts:create`
`serviceaccounts:write`
`serviceaccounts:delete`
`serviceaccounts.permissions:read`
`serviceaccounts.permissions:write` | Create, update, read and delete all Grafana service accounts and manage service account permissions. | +| `fixed:settings:reader` | `settings:read` | Read Grafana instance settings. | +| `fixed:settings:writer` | All permissions from `fixed:settings:reader` and
`settings:write` | Read and update Grafana instance settings. | +| `fixed:stats:reader` | `server.stats:read` | Read Grafana instance statistics. | +| `fixed:teams:creator` | `teams:create`
`org.users:read` | Create a team and list organization users (required to manage the created team). | +| `fixed:teams:writer` | `teams:create`
`teams:delete`
`teams:read`
`teams:write`
`teams.permissions:read`
`teams.permissions:write` | Create, read, update and delete teams and manage team memberships. | +| `fixed:users:reader` | `users:read`
`users.quotas:read`
`users.authtoken:read`
` | Read all users and their information, such as team memberships, authentication tokens, and quotas. | +| `fixed:users:writer` | All permissions from `fixed:users:reader` and
`users:write`
`users:create`
`users:delete`
`users:enable`
`users:disable`
`users.password:write`
`users.permissions:write`
`users:logout`
`users.authtoken:write`
`users.quotas:write` | Read and update all attributes and settings for all users in Grafana: update user information, read user information, create or enable or disable a user, make a user a Grafana administrator, sign out a user, update a user’s authentication token, or update quotas for all users. | ### Alerting roles diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index 0de2480798c..3154ecfbc5d 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -455,8 +455,9 @@ const ( ActionAlertingNotificationsExternalRead = "alert.notifications.external:read" // Alerting provisioning actions - ActionAlertingProvisioningRead = "alert.provisioning:read" - ActionAlertingProvisioningWrite = "alert.provisioning:write" + ActionAlertingProvisioningRead = "alert.provisioning:read" + ActionAlertingProvisioningReadSecrets = "alert.provisioning.secrets:read" + ActionAlertingProvisioningWrite = "alert.provisioning:write" // Feature Management actions ActionFeatureManagementRead = "featuremgmt.read" diff --git a/pkg/services/ngalert/accesscontrol.go b/pkg/services/ngalert/accesscontrol.go index f583a9ae47a..c946f8ea15e 100644 --- a/pkg/services/ngalert/accesscontrol.go +++ b/pkg/services/ngalert/accesscontrol.go @@ -171,6 +171,24 @@ var ( }, Grants: []string{string(org.RoleAdmin)}, } + + alertingProvisioningReaderWithSecretsRole = accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: accesscontrol.FixedRolePrefix + "alerting.provisioning.secrets:reader", + DisplayName: "Read via Provisioning API + Export Secrets", + Description: "Read all alert rules, contact points, notification policies, silences, etc. in the organization via provisioning API and use export with decrypted secrets", + Group: AlertRolesGroup, + Permissions: []accesscontrol.Permission{ + { + Action: accesscontrol.ActionAlertingProvisioningReadSecrets, // organization scope + }, + { + Action: accesscontrol.ActionAlertingProvisioningRead, // organization scope + }, + }, + }, + Grants: []string{string(org.RoleAdmin)}, + } ) func DeclareFixedRoles(service accesscontrol.Service) error { @@ -178,6 +196,6 @@ func DeclareFixedRoles(service accesscontrol.Service) error { rulesReaderRole, rulesWriterRole, instancesReaderRole, instancesWriterRole, notificationsReaderRole, notificationsWriterRole, - alertingReaderRole, alertingWriterRole, alertingProvisionerRole, + alertingReaderRole, alertingWriterRole, alertingProvisionerRole, alertingProvisioningReaderWithSecretsRole, ) } diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index be7a8c47c3d..23c83cd7bbe 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -20,6 +20,8 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/log/logtest" + "github.com/grafana/grafana/pkg/services/accesscontrol" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" @@ -27,7 +29,6 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/notifier" "github.com/grafana/grafana/pkg/services/ngalert/provisioning" "github.com/grafana/grafana/pkg/services/ngalert/store" - "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/secrets" secrets_fakes "github.com/grafana/grafana/pkg/services/secrets/fakes" "github.com/grafana/grafana/pkg/services/user" @@ -1008,8 +1009,15 @@ func TestProvisioningApiContactPointExport(t *testing.T) { require.Equal(t, "", rc.Context.Resp.Header().Get("Content-Disposition")) }) - t.Run("decrypt true without admin returns 403", func(t *testing.T) { - sut := createProvisioningSrvSut(t) + t.Run("decrypt true without alert.provisioning.secrets:read permissions returns 403", func(t *testing.T) { + env := createTestEnv(t, testConfig) + env.ac = &recordingAccessControlFake{ + Callback: func(user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) { + return false, nil + }, + } + + sut := createProvisioningSrvSutFromEnv(t, &env) rc := createTestRequestCtx() rc.Context.Req.Form.Set("decrypt", "true") @@ -1017,19 +1025,30 @@ func TestProvisioningApiContactPointExport(t *testing.T) { response := sut.RouteGetContactPointsExport(&rc) require.Equal(t, 403, response.Status()) + require.Len(t, env.ac.EvaluateRecordings, 1) + require.Equal(t, accesscontrol.ActionAlertingProvisioningReadSecrets, env.ac.EvaluateRecordings[0].Evaluator.String()) }) t.Run("decrypt true with admin returns 200", func(t *testing.T) { - sut := createProvisioningSrvSut(t) + env := createTestEnv(t, testConfig) + env.ac = &recordingAccessControlFake{ + Callback: func(user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) { + require.Equal(t, accesscontrol.ActionAlertingProvisioningReadSecrets, evaluator.String()) + return true, nil + }, + } + + sut := createProvisioningSrvSutFromEnv(t, &env) rc := createTestRequestCtx() - rc.SignedInUser.OrgRole = org.RoleAdmin rc.Context.Req.Form.Set("decrypt", "true") response := sut.RouteGetContactPointsExport(&rc) response.WriteTo(&rc) require.Equal(t, 200, response.Status()) + require.Len(t, env.ac.EvaluateRecordings, 1) + require.Equal(t, accesscontrol.ActionAlertingProvisioningReadSecrets, env.ac.EvaluateRecordings[0].Evaluator.String()) }) t.Run("json body content is as expected", func(t *testing.T) { @@ -1061,10 +1080,12 @@ func TestProvisioningApiContactPointExport(t *testing.T) { }) t.Run("decrypt true", func(t *testing.T) { env := createTestEnv(t, testContactPointConfig) + env.ac.Callback = func(user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) { + return true, nil + } sut := createProvisioningSrvSutFromEnv(t, &env) rc := createTestRequestCtx() - rc.SignedInUser.OrgRole = org.RoleAdmin rc.Context.Req.Header.Add("Accept", "application/json") rc.Context.Req.Form.Set("decrypt", "true") @@ -1119,10 +1140,12 @@ func TestProvisioningApiContactPointExport(t *testing.T) { }) t.Run("decrypt true", func(t *testing.T) { env := createTestEnv(t, testContactPointConfig) + env.ac.Callback = func(user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) { + return true, nil + } sut := createProvisioningSrvSutFromEnv(t, &env) rc := createTestRequestCtx() - rc.SignedInUser.OrgRole = org.RoleAdmin rc.Context.Req.Header.Add("Accept", "application/yaml") rc.Context.Req.Form.Set("decrypt", "true") @@ -1160,6 +1183,7 @@ type testEnvironment struct { xact provisioning.TransactionManager quotas provisioning.QuotaChecker prov provisioning.ProvisioningStore + ac *recordingAccessControlFake } func createTestEnv(t *testing.T, testConfig string) testEnvironment { @@ -1212,6 +1236,8 @@ func createTestEnv(t *testing.T, testConfig string) testEnvironment { Title: "Folder Title2", }}, nil).Maybe() + ac := &recordingAccessControlFake{} + return testEnvironment{ secrets: secretsService, log: log, @@ -1221,6 +1247,7 @@ func createTestEnv(t *testing.T, testConfig string) testEnvironment { xact: xact, prov: prov, quotas: quotas, + ac: ac, } } @@ -1237,7 +1264,7 @@ func createProvisioningSrvSutFromEnv(t *testing.T, env *testEnvironment) Provisi return ProvisioningSrv{ log: env.log, policies: newFakeNotificationPolicyService(), - contactPointService: provisioning.NewContactPointService(env.configs, env.secrets, env.prov, env.xact, env.log), + contactPointService: provisioning.NewContactPointService(env.configs, env.secrets, env.prov, env.xact, env.log, env.ac), templates: provisioning.NewTemplateService(env.configs, env.prov, env.xact, env.log), muteTimings: provisioning.NewMuteTimingService(env.configs, env.prov, env.xact, env.log), alertRules: provisioning.NewAlertRuleService(env.store, env.prov, env.dashboardService, env.quotas, env.xact, 60, 10, env.log), @@ -1256,6 +1283,7 @@ func createTestRequestCtx() contextmodel.ReqContext { SignedInUser: &user.SignedInUser{ OrgID: 1, }, + Logger: &logtest.Fake{}, } } diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go index b322e4161d8..2824480e32f 100644 --- a/pkg/services/ngalert/api/authorization.go +++ b/pkg/services/ngalert/api/authorization.go @@ -198,7 +198,7 @@ func (api *API) authorize(method, path string) web.Handler { http.MethodGet + "/api/v1/provisioning/alert-rules/{UID}/export", http.MethodGet + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}", http.MethodGet + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export": - eval = ac.EvalPermission(ac.ActionAlertingProvisioningRead) // organization scope + eval = ac.EvalAny(ac.EvalPermission(ac.ActionAlertingProvisioningRead), ac.EvalPermission(ac.ActionAlertingProvisioningReadSecrets)) // organization scope case http.MethodPut + "/api/v1/provisioning/policies", http.MethodDelete + "/api/v1/provisioning/policies", diff --git a/pkg/services/ngalert/api/testing.go b/pkg/services/ngalert/api/testing.go index d2a72db8529..44d6c2be23e 100644 --- a/pkg/services/ngalert/api/testing.go +++ b/pkg/services/ngalert/api/testing.go @@ -1,6 +1,7 @@ package api import ( + "context" "fmt" "sync" "testing" @@ -8,8 +9,10 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/state" + "github.com/grafana/grafana/pkg/services/user" ) type fakeAlertInstanceManager struct { @@ -103,3 +106,34 @@ func (f *fakeAlertInstanceManager) GenerateAlertInstances(orgID int64, alertRule f.states[orgID][alertRuleUID] = append(f.states[orgID][alertRuleUID], newState) } } + +type recordingAccessControlFake struct { + Disabled bool + EvaluateRecordings []struct { + User *user.SignedInUser + Evaluator accesscontrol.Evaluator + } + Callback func(user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) +} + +func (a *recordingAccessControlFake) Evaluate(ctx context.Context, u *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) { + a.EvaluateRecordings = append(a.EvaluateRecordings, struct { + User *user.SignedInUser + Evaluator accesscontrol.Evaluator + }{User: u, Evaluator: evaluator}) + if a.Callback == nil { + return false, nil + } + return a.Callback(u, evaluator) +} + +func (a *recordingAccessControlFake) RegisterScopeAttributeResolver(prefix string, resolver accesscontrol.ScopeAttributeResolver) { + // TODO implement me + panic("implement me") +} + +func (a *recordingAccessControlFake) IsDisabled() bool { + return a.Disabled +} + +var _ accesscontrol.AccessControl = &recordingAccessControlFake{} diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 3b2f0844fa5..37ef2f12e60 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -234,7 +234,7 @@ func (ng *AlertNG) init() error { // Provisioning policyService := provisioning.NewNotificationPolicyService(ng.store, ng.store, ng.store, ng.Cfg.UnifiedAlerting, ng.Log) - contactPointService := provisioning.NewContactPointService(ng.store, ng.SecretsService, ng.store, ng.store, ng.Log) + contactPointService := provisioning.NewContactPointService(ng.store, ng.SecretsService, ng.store, ng.store, ng.Log, ng.accesscontrol) templateService := provisioning.NewTemplateService(ng.store, ng.store, ng.store, ng.Log) muteTimingService := provisioning.NewMuteTimingService(ng.store, ng.store, ng.store, ng.Log) alertRuleService := provisioning.NewAlertRuleService(ng.store, ng.store, ng.dashboardService, ng.QuotaService, ng.store, diff --git a/pkg/services/ngalert/provisioning/contactpoints.go b/pkg/services/ngalert/provisioning/contactpoints.go index d083a410ff5..21b9df92cc8 100644 --- a/pkg/services/ngalert/provisioning/contactpoints.go +++ b/pkg/services/ngalert/provisioning/contactpoints.go @@ -13,10 +13,10 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/accesscontrol" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/notifier/channels_config" - "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util" @@ -28,16 +28,18 @@ type ContactPointService struct { provenanceStore ProvisioningStore xact TransactionManager log log.Logger + ac accesscontrol.AccessControl } func NewContactPointService(store AMConfigStore, encryptionService secrets.Service, - provenanceStore ProvisioningStore, xact TransactionManager, log log.Logger) *ContactPointService { + provenanceStore ProvisioningStore, xact TransactionManager, log log.Logger, ac accesscontrol.AccessControl) *ContactPointService { return &ContactPointService{ amStore: store, encryptionService: encryptionService, provenanceStore: provenanceStore, xact: xact, log: log, + ac: ac, } } @@ -49,10 +51,22 @@ type ContactPointQuery struct { Decrypt bool } +func (ecp *ContactPointService) canDecryptSecrets(ctx context.Context, u *user.SignedInUser) bool { + if u == nil { + return false + } + permitted, err := ecp.ac.Evaluate(ctx, u, accesscontrol.EvalPermission(accesscontrol.ActionAlertingProvisioningReadSecrets)) + if err != nil { + ecp.log.Error("Failed to evaluate user permissions", "error", err) + permitted = false + } + return permitted +} + // GetContactPoints returns contact points. If q.Decrypt is true and the user is an OrgAdmin, decrypted secure settings are included instead of redacted ones. func (ecp *ContactPointService) GetContactPoints(ctx context.Context, q ContactPointQuery, u *user.SignedInUser) ([]apimodels.EmbeddedContactPoint, error) { - if q.Decrypt && (u == nil || !u.HasRole(org.RoleAdmin)) { - return nil, fmt.Errorf("%w: decrypting secure settings requires Org Admin", ErrPermissionDenied) + if q.Decrypt && !ecp.canDecryptSecrets(ctx, u) { + return nil, fmt.Errorf("%w: user requires Admin role or alert.provisioning.secrets:read permission to view decrypted secure settings", ErrPermissionDenied) } revision, err := getLastConfiguration(ctx, q.OrgID, ecp.amStore) if err != nil { diff --git a/pkg/services/ngalert/provisioning/contactpoints_test.go b/pkg/services/ngalert/provisioning/contactpoints_test.go index 37d13e0f5d8..d92dfa8bd00 100644 --- a/pkg/services/ngalert/provisioning/contactpoints_test.go +++ b/pkg/services/ngalert/provisioning/contactpoints_test.go @@ -12,14 +12,17 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" + "github.com/grafana/grafana/pkg/services/accesscontrol/actest" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/notifier" - "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets/database" "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" ) func TestContactPointService(t *testing.T) { @@ -243,6 +246,7 @@ func TestContactPointService(t *testing.T) { func TestContactPointServiceDecryptRedact(t *testing.T) { sqlStore := db.InitTestDB(t) secretsService := manager.SetupTestService(t, database.ProvideSecretsStore(sqlStore)) + ac := acimpl.ProvideAccessControl(setting.NewCfg()) t.Run("GetContactPoints gets redacted contact points by default", func(t *testing.T) { sut := createContactPointServiceSut(t, secretsService) @@ -253,20 +257,36 @@ func TestContactPointServiceDecryptRedact(t *testing.T) { require.Equal(t, "slack receiver", cps[0].Name) require.Equal(t, definitions.RedactedValue, cps[0].Settings.Get("url").MustString()) }) - t.Run("GetContactPoints errors when Decrypt = true and user not Org Admin", func(t *testing.T) { + t.Run("GetContactPoints errors when Decrypt = true and user does not have permissions", func(t *testing.T) { sut := createContactPointServiceSut(t, secretsService) + sut.ac = ac + + q := cpsQuery(1) + q.Decrypt = true + _, err := sut.GetContactPoints(context.Background(), q, &user.SignedInUser{}) + require.ErrorIs(t, err, ErrPermissionDenied) + }) + t.Run("GetContactPoints errors when Decrypt = true and user is nil", func(t *testing.T) { + sut := createContactPointServiceSut(t, secretsService) + sut.ac = ac q := cpsQuery(1) q.Decrypt = true _, err := sut.GetContactPoints(context.Background(), q, nil) require.ErrorIs(t, err, ErrPermissionDenied) }) - t.Run("GetContactPoints gets decrypted contact points when Decrypt = true and user is Org Admin", func(t *testing.T) { + + t.Run("GetContactPoints gets decrypted contact points when Decrypt = true and user has permissions", func(t *testing.T) { sut := createContactPointServiceSut(t, secretsService) + sut.ac = ac q := cpsQuery(1) q.Decrypt = true - cps, err := sut.GetContactPoints(context.Background(), q, &user.SignedInUser{OrgID: 1, OrgRole: org.RoleAdmin}) + cps, err := sut.GetContactPoints(context.Background(), q, &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{ + 1: { + accesscontrol.ActionAlertingProvisioningReadSecrets: nil, + }, + }}) require.NoError(t, err) require.Len(t, cps, 1) @@ -325,6 +345,7 @@ func createContactPointServiceSut(t *testing.T, secretService secrets.Service) * xact: newNopTransactionManager(), encryptionService: secretService, log: log.NewNopLogger(), + ac: actest.FakeAccessControl{}, } } diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index 6496c9bbb90..9e490635456 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -276,7 +276,7 @@ func (ps *ProvisioningServiceImpl) ProvisionAlerting(ctx context.Context) error int64(ps.Cfg.UnifiedAlerting.BaseInterval.Seconds()), ps.log) contactPointService := provisioning.NewContactPointService(&st, ps.secretService, - st, ps.SQLStore, ps.log) + st, ps.SQLStore, ps.log, ps.ac) notificationPolicyService := provisioning.NewNotificationPolicyService(&st, st, ps.SQLStore, ps.Cfg.UnifiedAlerting, ps.log) mutetimingsService := provisioning.NewMuteTimingService(&st, st, &st, ps.log) diff --git a/public/app/features/alerting/unified/RuleList.test.tsx b/public/app/features/alerting/unified/RuleList.test.tsx index 5b8a50c1691..804a89759d1 100644 --- a/public/app/features/alerting/unified/RuleList.test.tsx +++ b/public/app/features/alerting/unified/RuleList.test.tsx @@ -696,6 +696,21 @@ describe('RuleList', () => { await userEvent.click(ui.moreButton.get()); expect(ui.exportButton.get()).toBeInTheDocument(); }); + it('Export button should be visible when the user has alert provisioning read secrets permissions', async () => { + enableRBAC(); + + grantUserPermissions([AccessControlAction.AlertingProvisioningReadSecrets]); + + mocks.getAllDataSourcesMock.mockReturnValue([]); + setDataSourceSrv(new MockDataSourceSrv({})); + mocks.api.fetchRules.mockResolvedValue([]); + mocks.api.fetchRulerRules.mockResolvedValue({}); + + renderRuleList(); + + await userEvent.click(ui.moreButton.get()); + expect(ui.exportButton.get()).toBeInTheDocument(); + }); it('Export button should not be visible when the user has no alert provisioning read permissions', async () => { enableRBAC(); diff --git a/public/app/features/alerting/unified/components/notification-policies/Policy.tsx b/public/app/features/alerting/unified/components/notification-policies/Policy.tsx index ff87815229f..4466a13f86c 100644 --- a/public/app/features/alerting/unified/components/notification-policies/Policy.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/Policy.tsx @@ -74,7 +74,9 @@ const Policy: FC = ({ const permissions = getNotificationsPermissions(alertManagerSourceName); const canEditRoutes = contextSrv.hasPermission(permissions.update); const canDeleteRoutes = contextSrv.hasPermission(permissions.delete); - const canReadProvisioning = contextSrv.hasAccess(permissions.provisioning.read, isOrgAdmin()); + const canReadProvisioning = + contextSrv.hasAccess(permissions.provisioning.read, isOrgAdmin()) || + contextSrv.hasPermission(permissions.provisioning.readSecrets); const contactPoint = currentRoute.receiver; const continueMatching = currentRoute.continue ?? false; diff --git a/public/app/features/alerting/unified/components/receivers/ReceiversTable.test.tsx b/public/app/features/alerting/unified/components/receivers/ReceiversTable.test.tsx index 99ec8e3066a..0198aff5eac 100644 --- a/public/app/features/alerting/unified/components/receivers/ReceiversTable.test.tsx +++ b/public/app/features/alerting/unified/components/receivers/ReceiversTable.test.tsx @@ -8,16 +8,23 @@ import { Receiver, } from 'app/plugins/datasource/alertmanager/types'; import { configureStore } from 'app/store/configureStore'; -import { ContactPointsState, NotifierDTO, NotifierType } from 'app/types'; +import { AccessControlAction, ContactPointsState, NotifierDTO, NotifierType } from 'app/types'; import * as onCallApi from '../../api/onCallApi'; import * as receiversApi from '../../api/receiversApi'; +import { enableRBAC, grantUserPermissions } from '../../mocks'; import { fetchGrafanaNotifiersAction } from '../../state/actions'; +import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; +import { createUrl } from '../../utils/url'; import { ReceiversTable } from './ReceiversTable'; import * as grafanaApp from './grafanaAppReceivers/grafanaApp'; -const renderReceieversTable = async (receivers: Receiver[], notifiers: NotifierDTO[]) => { +const renderReceieversTable = async ( + receivers: Receiver[], + notifiers: NotifierDTO[], + alertmanagerName = 'alertmanager-1' +) => { const config: AlertManagerCortexConfig = { template_files: {}, alertmanager_config: { @@ -30,7 +37,7 @@ const renderReceieversTable = async (receivers: Receiver[], notifiers: NotifierD return render( - + ); }; @@ -127,4 +134,88 @@ describe('ReceiversTable', () => { expect(rows[1]).toHaveTextContent('without receivers'); expect(rows[1].querySelector('[data-column="Type"]')).toHaveTextContent(''); }); + + describe('RBAC Enabled', () => { + describe('Export button', () => { + const receivers: Receiver[] = [ + { + name: 'with receivers', + grafana_managed_receiver_configs: [mockGrafanaReceiver('googlechat'), mockGrafanaReceiver('sensugo')], + }, + { + name: 'no receivers', + }, + ]; + + const notifiers: NotifierDTO[] = [mockNotifier('googlechat', 'Google Chat'), mockNotifier('sensugo', 'Sensu Go')]; + + it('should be visible when user has permissions to read provisioning', async () => { + enableRBAC(); + grantUserPermissions([AccessControlAction.AlertingProvisioningRead]); + + await renderReceieversTable(receivers, notifiers, GRAFANA_RULES_SOURCE_NAME); + + const buttons = within(screen.getByTestId('dynamic-table')).getAllByTestId('export'); + expect(buttons).toHaveLength(2); + expect(buttons).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + href: createUrl(`http://localhost/api/v1/provisioning/contact-points/export/`, { + download: 'true', + format: 'yaml', + decrypt: 'false', + name: 'with receivers', + }), + }), + expect.objectContaining({ + href: createUrl(`http://localhost/api/v1/provisioning/contact-points/export/`, { + download: 'true', + format: 'yaml', + decrypt: 'false', + name: 'no receivers', + }), + }), + ]) + ); + }); + it('should be visible when user has permissions to read provisioning with secrets', async () => { + enableRBAC(); + grantUserPermissions([AccessControlAction.AlertingProvisioningReadSecrets]); + + await renderReceieversTable(receivers, notifiers, GRAFANA_RULES_SOURCE_NAME); + + const buttons = within(screen.getByTestId('dynamic-table')).getAllByTestId('export'); + expect(buttons).toHaveLength(2); + expect(buttons).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + href: createUrl(`http://localhost/api/v1/provisioning/contact-points/export/`, { + download: 'true', + format: 'yaml', + decrypt: 'true', + name: 'with receivers', + }), + }), + expect.objectContaining({ + href: createUrl(`http://localhost/api/v1/provisioning/contact-points/export/`, { + download: 'true', + format: 'yaml', + decrypt: 'true', + name: 'no receivers', + }), + }), + ]) + ); + }); + it('should not be visible when user has no provisioning permissions', async () => { + enableRBAC(); + grantUserPermissions([AccessControlAction.AlertingNotificationsRead]); + + await renderReceieversTable(receivers, [], GRAFANA_RULES_SOURCE_NAME); + + const buttons = within(screen.getByTestId('dynamic-table')).queryAllByTestId('export'); + expect(buttons).toHaveLength(0); + }); + }); + }); }); diff --git a/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx b/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx index 7e111fd8dfb..8959240423b 100644 --- a/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx +++ b/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx @@ -69,6 +69,7 @@ interface ActionProps { delete: AccessControlAction; provisioning: { read: AccessControlAction; + readSecrets: AccessControlAction; }; }; alertManagerName: string; @@ -89,17 +90,20 @@ function ViewAction({ permissions, alertManagerName, receiverName }: ActionProps } function ExportAction({ permissions, receiverName }: ActionProps) { + const canReadSecrets = contextSrv.hasAccess(permissions.provisioning.readSecrets, isOrgAdmin()); return ( - + @@ -301,7 +305,10 @@ export const ReceiversTable = ({ config, alertManagerName }: Props) => { const [showCannotDeleteReceiverModal, setShowCannotDeleteReceiverModal] = useState(false); const isGrafanaAM = alertManagerName === GRAFANA_RULES_SOURCE_NAME; - const showExport = isGrafanaAM && contextSrv.hasAccess(permissions.provisioning.read, isOrgAdmin()); + const showExport = + isGrafanaAM && + (contextSrv.hasAccess(permissions.provisioning.read, isOrgAdmin()) || + contextSrv.hasAccess(permissions.provisioning.readSecrets, isOrgAdmin())); const onClickDeleteReceiver = (receiverName: string): void => { if (isReceiverUsed(receiverName, config)) { @@ -436,6 +443,7 @@ function useGetColumns( delete: AccessControlAction; provisioning: { read: AccessControlAction; + readSecrets: AccessControlAction; }; }, isVanillaAM: boolean @@ -498,7 +506,12 @@ function useGetColumns( label: 'Actions', renderCell: ({ data: { provisioned, name } }) => (
diff --git a/public/app/features/alerting/unified/utils/access-control.ts b/public/app/features/alerting/unified/utils/access-control.ts index 56f4612e22d..b82631481b2 100644 --- a/public/app/features/alerting/unified/utils/access-control.ts +++ b/public/app/features/alerting/unified/utils/access-control.ts @@ -50,6 +50,7 @@ export const notificationsPermissions = { export const provisioningPermissions = { read: AccessControlAction.AlertingProvisioningRead, + readSecrets: AccessControlAction.AlertingProvisioningReadSecrets, write: AccessControlAction.AlertingProvisioningWrite, }; @@ -125,6 +126,8 @@ export function getRulesAccess() { rulesSourceName === GRAFANA_RULES_SOURCE_NAME ? contextSrv.hasEditPermissionInFolders : contextSrv.isEditor; return contextSrv.hasAccess(getRulesPermissions(rulesSourceName).update, permissionFallback); }, - canReadProvisioning: contextSrv.hasAccess(provisioningPermissions.read, isOrgAdmin()), + canReadProvisioning: + contextSrv.hasAccess(provisioningPermissions.read, isOrgAdmin()) || + contextSrv.hasAccess(provisioningPermissions.readSecrets, isOrgAdmin()), }; } diff --git a/public/app/types/accessControl.ts b/public/app/types/accessControl.ts index 16e7a96165e..93e3a34e49f 100644 --- a/public/app/types/accessControl.ts +++ b/public/app/types/accessControl.ts @@ -118,6 +118,7 @@ export enum AccessControlAction { AlertingNotificationsExternalRead = 'alert.notifications.external:read', // Alerting provisioning actions + AlertingProvisioningReadSecrets = 'alert.provisioning.secrets:read', AlertingProvisioningRead = 'alert.provisioning:read', AlertingProvisioningWrite = 'alert.provisioning:write',