RBAC: Allow listing user permissions with scope (#57538)

* RBAC: Allow listing user permissions with scope

* Add docs

* Document the api endpoint

* Update docs

Co-authored-by: Garrett Guillotte <100453168+gguillotte-grafana@users.noreply.github.com>

* Split endpoint in two

* document reloadcache

* Update docs/sources/developers/http_api/access_control.md

* Fix test

* Ieva's nit.

* Simplify flag description

Co-authored-by: Garrett Guillotte <100453168+gguillotte-grafana@users.noreply.github.com>
This commit is contained in:
Gabriel MABILLE
2022-11-02 10:48:11 +01:00
committed by GitHub
parent f1f0a6f88b
commit 101ce57a94
5 changed files with 247 additions and 6 deletions

View File

@ -153,3 +153,63 @@ You can assign on of the following permissions to a specific user or a team:
1. In the **Permissions** section at the bottom, click **Add permission**. 1. In the **Permissions** section at the bottom, click **Add permission**.
1. Choose **User** in the dropdown and select your desired user. 1. Choose **User** in the dropdown and select your desired user.
1. Choose **View**, **Edit** or **Admin** role in the dropdown and click **Save**. 1. Choose **View**, **Edit** or **Admin** role in the dropdown and click **Save**.
## Debug the permissions of a service account token
This section explains how to learn which RBAC permissions are attached to a service account token.
This can help you diagnose permissions-related issues with token authorization.
### Before you begin
These endpoints provide details on a service account's token.
If you haven't added a token to a service account, do so before proceeding.
For details, refer to [Add a token to a service account]({{< relref "#add-a-token-to-a-service-account-in-grafana" >}}).
### List a service account token's permissions
To list your token's permissions, use the `/api/access-control/user/permissions` endpoint.
#### Example
> **Note:** The following command output is shortened to show only the relevant content.
> Authorize your request with the token whose permissions you want to check.
```bash
curl -H "Authorization: Bearer glsa_HOruNAb7SOiCdshU9algkrq7FDsNSLAa_54e2f8be" -X GET '<grafana_url>/api/access-control/user/permissions' | jq
```
The output lists the token's permissions:
```json
{
"dashboards:read": ["dashboards:uid:70KrY6IVz"],
"dashboards:write": ["dashboards:uid:70KrY6IVz"],
"datasources.id:read": ["datasources:*"],
"datasources:read": ["datasources:*"],
"datasources:explore": [""],
"datasources:query": ["datasources:uid:grafana"],
"datasources:read": ["datasources:uid:grafana"],
"orgs:read": [""]
}
```
### Check which dashboards a token is allowed to see
To list which dashboards a token can view, you can filter the `/api/access-control/user/permissions` endpoint's response for the `dashboards:read` permission key.
#### Example
```bash
curl -H "Authorization: Bearer glsa_HOruNAb7SOiCdshU9algkrq7FDsNSLAa_54e2f8be" -X GET '<grafana_url>/api/access-control/user/permissions' | jq '."dashboards:read"'
```
The output lists the dashboards a token can view and the folders a token can view dashboards from,
by their unique identifiers (`uid`):
```json
[
"dashboards:uid:70KrY6IVz",
"dashboards:uid:d61be733D",
"folders:uid:dBS87Axw2",
],
```

View File

@ -527,11 +527,60 @@ Content-Type: application/json; charset=UTF-8
`permissions:type:delegate` scope ensures that users can only unassign roles which have same, or a subset of permissions which the user has. `permissions:type:delegate` scope ensures that users can only unassign roles which have same, or a subset of permissions which the user has.
For example, if a user does not have required permissions for creating users, they won't be able to unassign a role which will allow to do that. This is done to prevent escalation of privileges. For example, if a user does not have required permissions for creating users, they won't be able to unassign a role which will allow to do that. This is done to prevent escalation of privileges.
| Action | Scope |
| ------------------ | ------------------------- |
| users.roles:remove | permissions:type:delegate |
#### Query parameters
| Param | Type | Required | Description |
| ------ | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| global | boolean | No | A flag indicating if the assignment is global or not. If set to `false`, the default org ID of the authenticated user will be used from the request to remove assignment. |
#### Example request
```http
DELETE /api/access-control/users/1/roles/AFUXBHKnk
Accept: application/json
```
#### Example response
```http
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
```
#### Status codes
| Code | Description |
| ---- | -------------------------------------------------------------------- |
| 200 | Role is unassigned. |
| 403 | Access denied. |
| 500 | Unexpected error. Refer to body and/or server logs for more details. |
### Set user role assignments
`PUT /api/access-control/users/:userId/roles`
Update the user's role assignments to match the provided set of UIDs.
This will remove any assigned roles that aren't in the request and add
roles that are in the set but are not already assigned to the user.
If you want to add or remove a single role, consider using
[Add a user role assignment]({{< ref "#add-a-user-role-assignment" >}}) or
[Remove a user role assignment]({{< ref "#remove-a-user-role-assignment" >}})
instead.
#### Required permissions
`permissions:type:delegate` scope ensures that users can only assign or unassign roles which have same, or a subset of permissions which the user has. `permissions:type:delegate` scope ensures that users can only assign or unassign roles which have same, or a subset of permissions which the user has.
For example, if a user does not have required permissions for creating users, they won't be able to assign or unassign a role which will allow to do that. This is done to prevent escalation of privileges. For example, if a user does not have required permissions for creating users, they won't be able to assign or unassign a role which will allow to do that. This is done to prevent escalation of privileges.
| Action | Scope | | Action | Scope |
[Add a user role assignment]({{< ref "#add-a-user-role-assignment" >}}) or | ------------------ | ------------------------- |
| users.roles:add | permissions:type:delegate | | users.roles:add | permissions:type:delegate |
| users.roles:remove | permissions:type:delegate | | users.roles:remove | permissions:type:delegate |

View File

@ -24,12 +24,14 @@ type AccessControlAPI struct {
func (api *AccessControlAPI) RegisterAPIEndpoints() { func (api *AccessControlAPI) RegisterAPIEndpoints() {
// Users // Users
api.RouteRegister.Get("/api/access-control/user/permissions", api.RouteRegister.Group("/api/access-control", func(rr routing.RouteRegister) {
middleware.ReqSignedIn, routing.Wrap(api.getUsersPermissions)) rr.Get("/user/actions", middleware.ReqSignedIn, routing.Wrap(api.getUserActions))
rr.Get("/user/permissions", middleware.ReqSignedIn, routing.Wrap(api.getUserPermissions))
})
} }
// GET /api/access-control/user/permissions // GET /api/access-control/user/actions
func (api *AccessControlAPI) getUsersPermissions(c *models.ReqContext) response.Response { func (api *AccessControlAPI) getUserActions(c *models.ReqContext) response.Response {
reloadCache := c.QueryBool("reloadcache") reloadCache := c.QueryBool("reloadcache")
permissions, err := api.Service.GetUserPermissions(c.Req.Context(), permissions, err := api.Service.GetUserPermissions(c.Req.Context(),
c.SignedInUser, ac.Options{ReloadCache: reloadCache}) c.SignedInUser, ac.Options{ReloadCache: reloadCache})
@ -39,3 +41,15 @@ func (api *AccessControlAPI) getUsersPermissions(c *models.ReqContext) response.
return response.JSON(http.StatusOK, ac.BuildPermissionsMap(permissions)) return response.JSON(http.StatusOK, ac.BuildPermissionsMap(permissions))
} }
// GET /api/access-control/user/permissions
func (api *AccessControlAPI) getUserPermissions(c *models.ReqContext) response.Response {
reloadCache := c.QueryBool("reloadcache")
permissions, err := api.Service.GetUserPermissions(c.Req.Context(),
c.SignedInUser, ac.Options{ReloadCache: reloadCache})
if err != nil {
response.JSON(http.StatusInternalServerError, err)
}
return response.JSON(http.StatusOK, ac.GroupScopesByAction(permissions))
}

View File

@ -0,0 +1,118 @@
package api
import (
"encoding/json"
"net/http"
"testing"
"github.com/grafana/grafana/pkg/api/routing"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web/webtest"
"github.com/stretchr/testify/require"
)
func TestAPI_getUserActions(t *testing.T) {
type testCase struct {
desc string
permissions []ac.Permission
expectedOutput util.DynMap
expectedCode int
}
tests := []testCase{
{
desc: "Should be able to get actions",
permissions: []ac.Permission{
{Action: datasources.ActionRead, Scope: datasources.ScopeAll},
{Action: datasources.ActionRead, Scope: datasources.ScopeProvider.GetResourceScope("aabbccdd")},
},
expectedOutput: util.DynMap{datasources.ActionRead: true},
expectedCode: http.StatusOK,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
acSvc := actest.FakeService{ExpectedPermissions: tt.permissions}
api := NewAccessControlAPI(routing.NewRouteRegister(), acSvc)
api.RegisterAPIEndpoints()
server := webtest.NewServer(t, api.RouteRegister)
url := "/api/access-control/user/actions"
req := server.NewGetRequest(url)
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{},
})
res, err := server.Send(req)
defer func() { require.NoError(t, res.Body.Close()) }()
require.NoError(t, err)
require.Equal(t, tt.expectedCode, res.StatusCode)
if tt.expectedCode == http.StatusOK {
var output util.DynMap
err := json.NewDecoder(res.Body).Decode(&output)
require.NoError(t, err)
require.Equal(t, tt.expectedOutput, output)
}
})
}
}
func TestAPI_getUserPermissions(t *testing.T) {
type testCase struct {
desc string
permissions []ac.Permission
expectedOutput util.DynMap
expectedCode int
}
tests := []testCase{
{
desc: "Should be able to get permissions with scope",
permissions: []ac.Permission{
{Action: datasources.ActionRead, Scope: datasources.ScopeAll},
{Action: datasources.ActionRead, Scope: datasources.ScopeProvider.GetResourceScope("aabbccdd")},
},
expectedOutput: util.DynMap{
datasources.ActionRead: []interface{}{
datasources.ScopeAll,
datasources.ScopeProvider.GetResourceScope("aabbccdd"),
}},
expectedCode: http.StatusOK,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
acSvc := actest.FakeService{ExpectedPermissions: tt.permissions}
api := NewAccessControlAPI(routing.NewRouteRegister(), acSvc)
api.RegisterAPIEndpoints()
server := webtest.NewServer(t, api.RouteRegister)
url := "/api/access-control/user/permissions"
req := server.NewGetRequest(url)
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{},
})
res, err := server.Send(req)
defer func() { require.NoError(t, res.Body.Close()) }()
require.NoError(t, err)
require.Equal(t, tt.expectedCode, res.StatusCode)
if tt.expectedCode == http.StatusOK {
var output util.DynMap
err := json.NewDecoder(res.Body).Decode(&output)
require.NoError(t, err)
require.Equal(t, tt.expectedOutput, output)
}
})
}
}

View File

@ -83,7 +83,7 @@ export class ContextSrv {
async fetchUserPermissions() { async fetchUserPermissions() {
try { try {
if (this.accessControlEnabled()) { if (this.accessControlEnabled()) {
this.user.permissions = await getBackendSrv().get('/api/access-control/user/permissions', { this.user.permissions = await getBackendSrv().get('/api/access-control/user/actions', {
reloadcache: true, reloadcache: true,
}); });
} }