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>
pull/57528/head
Gabriel MABILLE 3 years ago committed by GitHub
parent f1f0a6f88b
commit 101ce57a94
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 60
      docs/sources/administration/service-accounts/index.md
  2. 51
      docs/sources/developers/http_api/access_control.md
  3. 22
      pkg/services/accesscontrol/api/api.go
  4. 118
      pkg/services/accesscontrol/api/api_test.go
  5. 2
      public/app/core/services/context_srv.ts

@ -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. Choose **User** in the dropdown and select your desired user.
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",
],
```

@ -527,11 +527,60 @@ Content-Type: application/json; charset=UTF-8
| 403 | Access denied. |
| 500 | Unexpected error. Refer to body and/or server logs for more details. |
### List your permissions
`GET /api/access-control/users/permissions`
Lists the permissions granted to the signed in user.
#### Required permissions
No permission is required.
#### Query parameters
| Param | Type | Required | Description |
| ----------- | ------- | -------- | -------------------------------------- |
| reloadcache | boolean | No | A flag to reload the permission cache. |
#### Example request
```http
GET /api/access-control/user/permissions
Accept: application/json
```
#### Example response
```http
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
{
"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": [""]
}
```
#### Status codes
| Code | Description |
| ---- | -------------------------------------------------------------------- |
| 200 | Set of assigned permissions is returned. |
| 403 | Access denied. |
| 500 | Unexpected error. Refer to body and/or server logs for more details. |
### List permissions assigned to a user
`GET /api/access-control/users/:userId/permissions`
Lists the permissions that a given user has.
Lists the permissions granted to a given user.
#### Required permissions

@ -24,12 +24,14 @@ type AccessControlAPI struct {
func (api *AccessControlAPI) RegisterAPIEndpoints() {
// Users
api.RouteRegister.Get("/api/access-control/user/permissions",
middleware.ReqSignedIn, routing.Wrap(api.getUsersPermissions))
api.RouteRegister.Group("/api/access-control", func(rr routing.RouteRegister) {
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
func (api *AccessControlAPI) getUsersPermissions(c *models.ReqContext) response.Response {
// GET /api/access-control/user/actions
func (api *AccessControlAPI) getUserActions(c *models.ReqContext) response.Response {
reloadCache := c.QueryBool("reloadcache")
permissions, err := api.Service.GetUserPermissions(c.Req.Context(),
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))
}
// 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))
}

@ -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)
}
})
}
}

@ -83,7 +83,7 @@ export class ContextSrv {
async fetchUserPermissions() {
try {
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,
});
}

Loading…
Cancel
Save