mirror of
https://github.com/grafana/grafana.git
synced 2025-07-30 04:42:35 +08:00
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:
@ -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",
|
||||||
|
],
|
||||||
|
```
|
||||||
|
@ -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 |
|
||||||
|
|
||||||
|
@ -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))
|
||||||
|
}
|
||||||
|
118
pkg/services/accesscontrol/api/api_test.go
Normal file
118
pkg/services/accesscontrol/api/api_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user