From 101ce57a9462cc68f3c760ae1731ce362f5de19d Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Wed, 2 Nov 2022 10:48:11 +0100 Subject: [PATCH] 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> --- .../administration/service-accounts/index.md | 60 +++++++++ .../developers/http_api/access_control.md | 51 +++++++- pkg/services/accesscontrol/api/api.go | 22 +++- pkg/services/accesscontrol/api/api_test.go | 118 ++++++++++++++++++ public/app/core/services/context_srv.ts | 2 +- 5 files changed, 247 insertions(+), 6 deletions(-) create mode 100644 pkg/services/accesscontrol/api/api_test.go diff --git a/docs/sources/administration/service-accounts/index.md b/docs/sources/administration/service-accounts/index.md index 3a6927de265..7ed85d7a5a1 100644 --- a/docs/sources/administration/service-accounts/index.md +++ b/docs/sources/administration/service-accounts/index.md @@ -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 '/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 '/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", +], +``` diff --git a/docs/sources/developers/http_api/access_control.md b/docs/sources/developers/http_api/access_control.md index 20d9b6bec54..327b844c443 100644 --- a/docs/sources/developers/http_api/access_control.md +++ b/docs/sources/developers/http_api/access_control.md @@ -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 diff --git a/pkg/services/accesscontrol/api/api.go b/pkg/services/accesscontrol/api/api.go index f13ff071f25..04e90e276c4 100644 --- a/pkg/services/accesscontrol/api/api.go +++ b/pkg/services/accesscontrol/api/api.go @@ -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)) +} diff --git a/pkg/services/accesscontrol/api/api_test.go b/pkg/services/accesscontrol/api/api_test.go new file mode 100644 index 00000000000..3be8ee82c30 --- /dev/null +++ b/pkg/services/accesscontrol/api/api_test.go @@ -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) + } + }) + } +} diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index 490267661f3..c8511c5aaa9 100644 --- a/public/app/core/services/context_srv.ts +++ b/public/app/core/services/context_srv.ts @@ -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, }); }