mirror of https://github.com/grafana/grafana
Access control: Refactor managed permission system to create api and frontend components (#42540)
* Refactor resource permissions * Add frondend components for resource permissions Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com> Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com> Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com> Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>pull/43334/head^2
parent
9cf5623918
commit
c3ca2d214d
@ -1,109 +0,0 @@ |
||||
package accesscontrol |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
) |
||||
|
||||
type ResourceManager struct { |
||||
resource string |
||||
actions []string |
||||
validActions map[string]struct{} |
||||
store ResourceStore |
||||
validator ResourceValidator |
||||
} |
||||
|
||||
type ResourceValidator func(ctx context.Context, orgID int64, resourceID string) error |
||||
|
||||
func NewResourceManager(resource string, actions []string, validator ResourceValidator, store ResourceStore) *ResourceManager { |
||||
validActions := make(map[string]struct{}, len(actions)) |
||||
for _, a := range actions { |
||||
validActions[a] = struct{}{} |
||||
} |
||||
|
||||
return &ResourceManager{ |
||||
store: store, |
||||
actions: actions, |
||||
validActions: validActions, |
||||
resource: resource, |
||||
validator: validator, |
||||
} |
||||
} |
||||
|
||||
func (r *ResourceManager) GetPermissions(ctx context.Context, orgID int64, resourceID string) ([]ResourcePermission, error) { |
||||
return r.store.GetResourcesPermissions(ctx, orgID, GetResourcesPermissionsQuery{ |
||||
Actions: r.actions, |
||||
Resource: r.resource, |
||||
ResourceIDs: []string{resourceID}, |
||||
}) |
||||
} |
||||
|
||||
func (r *ResourceManager) GetPermissionsByIds(ctx context.Context, orgID int64, resourceIDs []string) ([]ResourcePermission, error) { |
||||
return r.store.GetResourcesPermissions(ctx, orgID, GetResourcesPermissionsQuery{ |
||||
Actions: r.actions, |
||||
Resource: r.resource, |
||||
ResourceIDs: resourceIDs, |
||||
}) |
||||
} |
||||
|
||||
func (r *ResourceManager) SetUserPermissions(ctx context.Context, orgID int64, resourceID string, actions []string, userID int64) ([]ResourcePermission, error) { |
||||
if !r.validateActions(actions) { |
||||
return nil, fmt.Errorf("invalid actions: %s", actions) |
||||
} |
||||
|
||||
return r.store.SetUserResourcePermissions(ctx, orgID, userID, SetResourcePermissionsCommand{ |
||||
Actions: actions, |
||||
Resource: r.resource, |
||||
ResourceID: resourceID, |
||||
}) |
||||
} |
||||
|
||||
func (r *ResourceManager) SetTeamPermission(ctx context.Context, orgID int64, resourceID string, actions []string, teamID int64) ([]ResourcePermission, error) { |
||||
if !r.validateActions(actions) { |
||||
return nil, fmt.Errorf("invalid action: %s", actions) |
||||
} |
||||
|
||||
return r.store.SetTeamResourcePermissions(ctx, orgID, teamID, SetResourcePermissionsCommand{ |
||||
Actions: actions, |
||||
Resource: r.resource, |
||||
ResourceID: resourceID, |
||||
}) |
||||
} |
||||
|
||||
func (r *ResourceManager) SetBuiltinRolePermissions(ctx context.Context, orgID int64, resourceID string, actions []string, builtinRole string) ([]ResourcePermission, error) { |
||||
if !r.validateActions(actions) { |
||||
return nil, fmt.Errorf("invalid action: %s", actions) |
||||
} |
||||
|
||||
return r.store.SetBuiltinResourcePermissions(ctx, orgID, builtinRole, SetResourcePermissionsCommand{ |
||||
Actions: actions, |
||||
Resource: r.resource, |
||||
ResourceID: resourceID, |
||||
}) |
||||
} |
||||
|
||||
func (r *ResourceManager) RemovePermission(ctx context.Context, orgID int64, resourceID string, permissionID int64) error { |
||||
return r.store.RemoveResourcePermission(ctx, orgID, RemoveResourcePermissionCommand{ |
||||
Actions: r.actions, |
||||
Resource: r.resource, |
||||
ResourceID: resourceID, |
||||
PermissionID: permissionID, |
||||
}) |
||||
} |
||||
|
||||
// Validate will run supplied ResourceValidator
|
||||
func (r *ResourceManager) Validate(ctx context.Context, orgID int64, resourceID string) error { |
||||
if r.validator != nil { |
||||
return r.validator(ctx, orgID, resourceID) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (r *ResourceManager) validateActions(actions []string) bool { |
||||
for _, a := range actions { |
||||
if _, ok := r.validActions[a]; !ok { |
||||
return false |
||||
} |
||||
} |
||||
return true |
||||
} |
||||
@ -0,0 +1,178 @@ |
||||
package resourcepermissions |
||||
|
||||
import ( |
||||
"fmt" |
||||
"net/http" |
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos" |
||||
"github.com/grafana/grafana/pkg/api/response" |
||||
"github.com/grafana/grafana/pkg/api/routing" |
||||
"github.com/grafana/grafana/pkg/models" |
||||
"github.com/grafana/grafana/pkg/services/accesscontrol" |
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/middleware" |
||||
"github.com/grafana/grafana/pkg/web" |
||||
) |
||||
|
||||
type api struct { |
||||
ac accesscontrol.AccessControl |
||||
router routing.RouteRegister |
||||
service *Service |
||||
permissions []string |
||||
} |
||||
|
||||
func newApi(ac accesscontrol.AccessControl, router routing.RouteRegister, manager *Service) *api { |
||||
permissions := make([]string, 0, len(manager.permissions)) |
||||
// reverse the permissions order for display
|
||||
for i := len(manager.permissions) - 1; i >= 0; i-- { |
||||
permissions = append(permissions, manager.permissions[i]) |
||||
} |
||||
return &api{ac, router, manager, permissions} |
||||
} |
||||
|
||||
func (a *api) registerEndpoints() { |
||||
auth := middleware.Middleware(a.ac) |
||||
disable := middleware.Disable(a.ac.IsDisabled()) |
||||
a.router.Group(fmt.Sprintf("/api/access-control/%s", a.service.options.Resource), func(r routing.RouteRegister) { |
||||
idScope := accesscontrol.Scope(a.service.options.Resource, "id", accesscontrol.Parameter(":resourceID")) |
||||
actionWrite, actionRead := fmt.Sprintf("%s.permissions:write", a.service.options.Resource), fmt.Sprintf("%s.permissions:read", a.service.options.Resource) |
||||
r.Get("/description", auth(disable, accesscontrol.EvalPermission(actionRead)), routing.Wrap(a.getDescription)) |
||||
r.Get("/:resourceID", auth(disable, accesscontrol.EvalPermission(actionRead, idScope)), routing.Wrap(a.getPermissions)) |
||||
r.Post("/:resourceID/users/:userID", auth(disable, accesscontrol.EvalPermission(actionWrite, idScope)), routing.Wrap(a.setUserPermission)) |
||||
r.Post("/:resourceID/teams/:teamID", auth(disable, accesscontrol.EvalPermission(actionWrite, idScope)), routing.Wrap(a.setTeamPermission)) |
||||
r.Post("/:resourceID/builtInRoles/:builtInRole", auth(disable, accesscontrol.EvalPermission(actionWrite, idScope)), routing.Wrap(a.setBuiltinRolePermission)) |
||||
}) |
||||
} |
||||
|
||||
type Assignments struct { |
||||
Users bool `json:"users"` |
||||
Teams bool `json:"teams"` |
||||
BuiltInRoles bool `json:"builtInRoles"` |
||||
} |
||||
|
||||
type Description struct { |
||||
Assignments Assignments `json:"assignments"` |
||||
Permissions []string `json:"permissions"` |
||||
} |
||||
|
||||
func (a *api) getDescription(c *models.ReqContext) response.Response { |
||||
return response.JSON(http.StatusOK, &Description{ |
||||
Permissions: a.permissions, |
||||
Assignments: a.service.options.Assignments, |
||||
}) |
||||
} |
||||
|
||||
type resourcePermissionDTO struct { |
||||
ID int64 `json:"id"` |
||||
ResourceID string `json:"resourceId"` |
||||
RoleName string `json:"roleName"` |
||||
IsManaged bool `json:"isManaged"` |
||||
UserID int64 `json:"userId,omitempty"` |
||||
UserLogin string `json:"userLogin,omitempty"` |
||||
UserAvatarUrl string `json:"userAvatarUrl,omitempty"` |
||||
Team string `json:"team,omitempty"` |
||||
TeamID int64 `json:"teamId,omitempty"` |
||||
TeamAvatarUrl string `json:"teamAvatarUrl,omitempty"` |
||||
BuiltInRole string `json:"builtInRole,omitempty"` |
||||
Actions []string `json:"actions"` |
||||
Permission string `json:"permission"` |
||||
} |
||||
|
||||
func (a *api) getPermissions(c *models.ReqContext) response.Response { |
||||
resourceID := web.Params(c.Req)[":resourceID"] |
||||
|
||||
permissions, err := a.service.GetPermissions(c.Req.Context(), c.OrgId, resourceID) |
||||
if err != nil { |
||||
return response.Error(http.StatusInternalServerError, "failed to get permissions", err) |
||||
} |
||||
|
||||
dto := make([]resourcePermissionDTO, 0, len(permissions)) |
||||
for _, p := range permissions { |
||||
if permission := a.service.MapActions(p); permission != "" { |
||||
teamAvatarUrl := "" |
||||
if p.TeamId != 0 { |
||||
teamAvatarUrl = dtos.GetGravatarUrlWithDefault(p.TeamEmail, p.Team) |
||||
} |
||||
|
||||
dto = append(dto, resourcePermissionDTO{ |
||||
ID: p.ID, |
||||
ResourceID: p.ResourceID, |
||||
RoleName: p.RoleName, |
||||
IsManaged: p.IsManaged(), |
||||
UserID: p.UserId, |
||||
UserLogin: p.UserLogin, |
||||
UserAvatarUrl: dtos.GetGravatarUrl(p.UserEmail), |
||||
Team: p.Team, |
||||
TeamID: p.TeamId, |
||||
TeamAvatarUrl: teamAvatarUrl, |
||||
BuiltInRole: p.BuiltInRole, |
||||
Actions: p.Actions, |
||||
Permission: permission, |
||||
}) |
||||
} |
||||
} |
||||
|
||||
return response.JSON(http.StatusOK, dto) |
||||
} |
||||
|
||||
type setPermissionCommand struct { |
||||
Permission string `json:"permission"` |
||||
} |
||||
|
||||
func (a *api) setUserPermission(c *models.ReqContext) response.Response { |
||||
userID := c.ParamsInt64(":userID") |
||||
resourceID := web.Params(c.Req)[":resourceID"] |
||||
|
||||
var cmd setPermissionCommand |
||||
if err := web.Bind(c.Req, &cmd); err != nil { |
||||
return response.Error(http.StatusBadRequest, "bad request data", err) |
||||
} |
||||
|
||||
_, err := a.service.SetUserPermission(c.Req.Context(), c.OrgId, userID, resourceID, a.service.MapPermission(cmd.Permission)) |
||||
if err != nil { |
||||
return response.Error(http.StatusBadRequest, "failed to set user permission", err) |
||||
} |
||||
|
||||
return permissionSetResponse(cmd) |
||||
} |
||||
|
||||
func (a *api) setTeamPermission(c *models.ReqContext) response.Response { |
||||
teamID := c.ParamsInt64(":teamID") |
||||
resourceID := web.Params(c.Req)[":resourceID"] |
||||
|
||||
var cmd setPermissionCommand |
||||
if err := web.Bind(c.Req, &cmd); err != nil { |
||||
return response.Error(http.StatusBadRequest, "bad request data", err) |
||||
} |
||||
|
||||
_, err := a.service.SetTeamPermission(c.Req.Context(), c.OrgId, teamID, resourceID, a.service.MapPermission(cmd.Permission)) |
||||
if err != nil { |
||||
return response.Error(http.StatusBadRequest, "failed to set team permission", err) |
||||
} |
||||
|
||||
return permissionSetResponse(cmd) |
||||
} |
||||
|
||||
func (a *api) setBuiltinRolePermission(c *models.ReqContext) response.Response { |
||||
builtInRole := web.Params(c.Req)[":builtInRole"] |
||||
resourceID := web.Params(c.Req)[":resourceID"] |
||||
|
||||
cmd := setPermissionCommand{} |
||||
if err := web.Bind(c.Req, &cmd); err != nil { |
||||
return response.Error(http.StatusBadRequest, "bad request data", err) |
||||
} |
||||
|
||||
_, err := a.service.SetBuiltInRolePermission(c.Req.Context(), c.OrgId, builtInRole, resourceID, a.service.MapPermission(cmd.Permission)) |
||||
if err != nil { |
||||
return response.Error(http.StatusBadRequest, "failed to set role permission", err) |
||||
} |
||||
|
||||
return permissionSetResponse(cmd) |
||||
} |
||||
|
||||
func permissionSetResponse(cmd setPermissionCommand) response.Response { |
||||
message := "Permission updated" |
||||
if cmd.Permission == "" { |
||||
message = "Permission removed" |
||||
} |
||||
return response.Success(message) |
||||
} |
||||
@ -0,0 +1,487 @@ |
||||
package resourcepermissions |
||||
|
||||
import ( |
||||
"context" |
||||
"encoding/json" |
||||
"fmt" |
||||
"net/http" |
||||
"net/http/httptest" |
||||
"path" |
||||
"strconv" |
||||
"strings" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
"github.com/stretchr/testify/require" |
||||
|
||||
"github.com/grafana/grafana/pkg/api/routing" |
||||
"github.com/grafana/grafana/pkg/infra/log" |
||||
"github.com/grafana/grafana/pkg/models" |
||||
"github.com/grafana/grafana/pkg/services/accesscontrol" |
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/database" |
||||
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" |
||||
"github.com/grafana/grafana/pkg/services/sqlstore" |
||||
"github.com/grafana/grafana/pkg/setting" |
||||
"github.com/grafana/grafana/pkg/web" |
||||
) |
||||
|
||||
type getDescriptionTestCase struct { |
||||
desc string |
||||
options Options |
||||
permissions []*accesscontrol.Permission |
||||
expected Description |
||||
expectedStatus int |
||||
} |
||||
|
||||
func TestApi_getDescription(t *testing.T) { |
||||
tests := []getDescriptionTestCase{ |
||||
{ |
||||
desc: "should return description", |
||||
options: Options{ |
||||
Resource: "dashboards", |
||||
Assignments: Assignments{ |
||||
Users: true, |
||||
Teams: true, |
||||
BuiltInRoles: true, |
||||
}, |
||||
PermissionsToActions: map[string][]string{ |
||||
"View": {"dashboards:read"}, |
||||
"Edit": {"dashboards:read", "dashboards:write", "dashboards:delete"}, |
||||
"Admin": {"dashboards:read", "dashboards:write", "dashboards:delete", "dashboards.permissions:read", "dashboards:permissions:write"}, |
||||
}, |
||||
}, |
||||
permissions: []*accesscontrol.Permission{ |
||||
{Action: "dashboards.permissions:read"}, |
||||
}, |
||||
expected: Description{ |
||||
Assignments: Assignments{ |
||||
Users: true, |
||||
Teams: true, |
||||
BuiltInRoles: true, |
||||
}, |
||||
Permissions: []string{"View", "Edit", "Admin"}, |
||||
}, |
||||
expectedStatus: http.StatusOK, |
||||
}, |
||||
{ |
||||
desc: "should only return user assignment", |
||||
options: Options{ |
||||
Resource: "dashboards", |
||||
Assignments: Assignments{ |
||||
Users: true, |
||||
Teams: false, |
||||
BuiltInRoles: false, |
||||
}, |
||||
PermissionsToActions: map[string][]string{ |
||||
"View": {"dashboards:read"}, |
||||
}, |
||||
}, |
||||
permissions: []*accesscontrol.Permission{ |
||||
{Action: "dashboards.permissions:read"}, |
||||
}, |
||||
expected: Description{ |
||||
Assignments: Assignments{ |
||||
Users: true, |
||||
Teams: false, |
||||
BuiltInRoles: false, |
||||
}, |
||||
Permissions: []string{"View"}, |
||||
}, |
||||
expectedStatus: http.StatusOK, |
||||
}, |
||||
{ |
||||
desc: "should return 403 when missing read permission", |
||||
options: Options{ |
||||
Resource: "dashboards", |
||||
Assignments: Assignments{ |
||||
Users: true, |
||||
Teams: false, |
||||
BuiltInRoles: false, |
||||
}, |
||||
PermissionsToActions: map[string][]string{ |
||||
"View": {"dashboards:read"}, |
||||
}, |
||||
}, |
||||
permissions: []*accesscontrol.Permission{}, |
||||
expected: Description{}, |
||||
expectedStatus: http.StatusForbidden, |
||||
}, |
||||
} |
||||
|
||||
for _, tt := range tests { |
||||
t.Run(tt.desc, func(t *testing.T) { |
||||
_, server, _ := setupTestEnvironment(t, &models.SignedInUser{}, tt.permissions, tt.options) |
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/api/access-control/%s/description", tt.options.Resource), nil) |
||||
require.NoError(t, err) |
||||
recorder := httptest.NewRecorder() |
||||
server.ServeHTTP(recorder, req) |
||||
|
||||
got := Description{} |
||||
require.NoError(t, json.NewDecoder(recorder.Body).Decode(&got)) |
||||
assert.Equal(t, tt.expected, got) |
||||
if tt.expectedStatus == http.StatusOK { |
||||
assert.Equal(t, tt.expectedStatus, recorder.Code) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
type getPermissionsTestCase struct { |
||||
desc string |
||||
resourceID string |
||||
permissions []*accesscontrol.Permission |
||||
expectedStatus int |
||||
} |
||||
|
||||
func TestApi_getPermissions(t *testing.T) { |
||||
tests := []getPermissionsTestCase{ |
||||
{ |
||||
desc: "expect permissions for resource with id 1", |
||||
resourceID: "1", |
||||
permissions: []*accesscontrol.Permission{{Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}}, |
||||
expectedStatus: 200, |
||||
}, |
||||
{ |
||||
desc: "expect http status 403 when missing permission", |
||||
resourceID: "1", |
||||
permissions: []*accesscontrol.Permission{}, |
||||
expectedStatus: 403, |
||||
}, |
||||
} |
||||
|
||||
for _, tt := range tests { |
||||
t.Run(tt.desc, func(t *testing.T) { |
||||
service, server, sql := setupTestEnvironment(t, &models.SignedInUser{OrgId: 1}, tt.permissions, testOptions) |
||||
|
||||
// seed team 1 with "Edit" permission on dashboard 1
|
||||
team, err := sql.CreateTeam("test", "test@test.com", 1) |
||||
require.NoError(t, err) |
||||
_, err = service.SetTeamPermission(context.Background(), team.OrgId, team.Id, tt.resourceID, []string{"dashboards:read", "dashboards:write", "dashboards:delete"}) |
||||
require.NoError(t, err) |
||||
// seed user 1 with "View" permission on dashboard 1
|
||||
u, err := sql.CreateUser(context.Background(), models.CreateUserCommand{Login: "test", OrgId: 1}) |
||||
require.NoError(t, err) |
||||
_, err = service.SetUserPermission(context.Background(), u.OrgId, u.Id, tt.resourceID, []string{"dashboards:read"}) |
||||
require.NoError(t, err) |
||||
|
||||
// seed built in role Admin with "View" permission on dashboard 1
|
||||
_, err = service.SetBuiltInRolePermission(context.Background(), 1, "Admin", tt.resourceID, []string{"dashboards:read", "dashboards:write", "dashboards:delete"}) |
||||
require.NoError(t, err) |
||||
|
||||
permissions, recorder := getPermission(t, server, testOptions.Resource, tt.resourceID) |
||||
assert.Equal(t, tt.expectedStatus, recorder.Code) |
||||
|
||||
if tt.expectedStatus == http.StatusOK { |
||||
assert.Len(t, permissions, 3, "expected three assignments: user, team, builtin") |
||||
for _, p := range permissions { |
||||
if p.UserID != 0 { |
||||
assert.Equal(t, "View", p.Permission) |
||||
} else if p.TeamID != 0 { |
||||
assert.Equal(t, "Edit", p.Permission) |
||||
} else { |
||||
assert.Equal(t, "Edit", p.Permission) |
||||
} |
||||
} |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
type setBuiltinPermissionTestCase struct { |
||||
desc string |
||||
resourceID string |
||||
builtInRole string |
||||
expectedStatus int |
||||
permission string |
||||
permissions []*accesscontrol.Permission |
||||
} |
||||
|
||||
func TestApi_setBuiltinRolePermission(t *testing.T) { |
||||
tests := []setBuiltinPermissionTestCase{ |
||||
{ |
||||
desc: "should set Edit permission for Viewer", |
||||
resourceID: "1", |
||||
builtInRole: "Viewer", |
||||
expectedStatus: 200, |
||||
permission: "Edit", |
||||
permissions: []*accesscontrol.Permission{ |
||||
{Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, |
||||
{Action: "dashboards.permissions:write", Scope: "dashboards:id:1"}, |
||||
}, |
||||
}, |
||||
{ |
||||
desc: "should set View permission for Admin", |
||||
resourceID: "1", |
||||
builtInRole: "Admin", |
||||
expectedStatus: 200, |
||||
permission: "View", |
||||
permissions: []*accesscontrol.Permission{ |
||||
{Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, |
||||
{Action: "dashboards.permissions:write", Scope: "dashboards:id:1"}, |
||||
}, |
||||
}, |
||||
{ |
||||
desc: "should return http 400 for invalid built in role", |
||||
resourceID: "1", |
||||
builtInRole: "Invalid", |
||||
expectedStatus: http.StatusBadRequest, |
||||
permission: "View", |
||||
permissions: []*accesscontrol.Permission{ |
||||
{Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, |
||||
{Action: "dashboards.permissions:write", Scope: "dashboards:id:1"}, |
||||
}, |
||||
}, |
||||
{ |
||||
desc: "should set return http 403 when missing permissions", |
||||
resourceID: "1", |
||||
builtInRole: "Invalid", |
||||
expectedStatus: http.StatusForbidden, |
||||
permission: "View", |
||||
permissions: []*accesscontrol.Permission{ |
||||
{Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
for _, tt := range tests { |
||||
t.Run(tt.desc, func(t *testing.T) { |
||||
_, server, _ := setupTestEnvironment(t, &models.SignedInUser{OrgId: 1}, tt.permissions, testOptions) |
||||
|
||||
recorder := setPermission(t, server, testOptions.Resource, tt.resourceID, tt.permission, "builtInRoles", tt.builtInRole) |
||||
assert.Equal(t, tt.expectedStatus, recorder.Code) |
||||
|
||||
if tt.expectedStatus == http.StatusOK { |
||||
permissions, _ := getPermission(t, server, testOptions.Resource, tt.resourceID) |
||||
require.Len(t, permissions, 1) |
||||
assert.Equal(t, tt.permission, permissions[0].Permission) |
||||
assert.Equal(t, tt.builtInRole, permissions[0].BuiltInRole) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
type setTeamPermissionTestCase struct { |
||||
desc string |
||||
teamID int64 |
||||
resourceID string |
||||
expectedStatus int |
||||
permission string |
||||
permissions []*accesscontrol.Permission |
||||
} |
||||
|
||||
func TestApi_setTeamPermission(t *testing.T) { |
||||
tests := []setTeamPermissionTestCase{ |
||||
{ |
||||
desc: "should set Edit permission for team 1", |
||||
teamID: 1, |
||||
resourceID: "1", |
||||
expectedStatus: 200, |
||||
permission: "Edit", |
||||
permissions: []*accesscontrol.Permission{ |
||||
{Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, |
||||
{Action: "dashboards.permissions:write", Scope: "dashboards:id:1"}, |
||||
}, |
||||
}, |
||||
{ |
||||
desc: "should set View permission for team 1", |
||||
teamID: 1, |
||||
resourceID: "1", |
||||
expectedStatus: 200, |
||||
permission: "View", |
||||
permissions: []*accesscontrol.Permission{ |
||||
{Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, |
||||
{Action: "dashboards.permissions:write", Scope: "dashboards:id:1"}, |
||||
}, |
||||
}, |
||||
{ |
||||
desc: "should set return http 400 when team does not exist", |
||||
teamID: 2, |
||||
resourceID: "1", |
||||
expectedStatus: http.StatusBadRequest, |
||||
permission: "View", |
||||
permissions: []*accesscontrol.Permission{ |
||||
{Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, |
||||
{Action: "dashboards.permissions:write", Scope: "dashboards:id:1"}, |
||||
}, |
||||
}, |
||||
{ |
||||
desc: "should set return http 403 when missing permissions", |
||||
teamID: 2, |
||||
resourceID: "1", |
||||
expectedStatus: http.StatusForbidden, |
||||
permission: "View", |
||||
permissions: []*accesscontrol.Permission{ |
||||
{Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
for _, tt := range tests { |
||||
t.Run(tt.desc, func(t *testing.T) { |
||||
_, server, sql := setupTestEnvironment(t, &models.SignedInUser{OrgId: 1}, tt.permissions, testOptions) |
||||
|
||||
// seed team
|
||||
_, err := sql.CreateTeam("test", "test@test.com", 1) |
||||
require.NoError(t, err) |
||||
|
||||
recorder := setPermission(t, server, testOptions.Resource, tt.resourceID, tt.permission, "teams", strconv.Itoa(int(tt.teamID))) |
||||
assert.Equal(t, tt.expectedStatus, recorder.Code) |
||||
|
||||
assert.Equal(t, tt.expectedStatus, recorder.Code) |
||||
if tt.expectedStatus == http.StatusOK { |
||||
permissions, _ := getPermission(t, server, testOptions.Resource, tt.resourceID) |
||||
require.Len(t, permissions, 1) |
||||
assert.Equal(t, tt.permission, permissions[0].Permission) |
||||
assert.Equal(t, tt.teamID, permissions[0].TeamID) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
type setUserPermissionTestCase struct { |
||||
desc string |
||||
userID int64 |
||||
resourceID string |
||||
expectedStatus int |
||||
permission string |
||||
permissions []*accesscontrol.Permission |
||||
} |
||||
|
||||
func TestApi_setUserPermission(t *testing.T) { |
||||
tests := []setUserPermissionTestCase{ |
||||
{ |
||||
desc: "should set Edit permission for user 1", |
||||
userID: 1, |
||||
resourceID: "1", |
||||
expectedStatus: 200, |
||||
permission: "Edit", |
||||
permissions: []*accesscontrol.Permission{ |
||||
{Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, |
||||
{Action: "dashboards.permissions:write", Scope: "dashboards:id:1"}, |
||||
}, |
||||
}, |
||||
{ |
||||
desc: "should set View permission for user 1", |
||||
userID: 1, |
||||
resourceID: "1", |
||||
expectedStatus: 200, |
||||
permission: "View", |
||||
permissions: []*accesscontrol.Permission{ |
||||
{Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, |
||||
{Action: "dashboards.permissions:write", Scope: "dashboards:id:1"}, |
||||
}, |
||||
}, |
||||
{ |
||||
desc: "should set return http 400 when user does not exist", |
||||
userID: 2, |
||||
resourceID: "1", |
||||
expectedStatus: http.StatusBadRequest, |
||||
permission: "View", |
||||
permissions: []*accesscontrol.Permission{ |
||||
{Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, |
||||
{Action: "dashboards.permissions:write", Scope: "dashboards:id:1"}, |
||||
}, |
||||
}, |
||||
{ |
||||
desc: "should set return http 403 when missing permissions", |
||||
userID: 2, |
||||
resourceID: "1", |
||||
expectedStatus: http.StatusForbidden, |
||||
permission: "View", |
||||
permissions: []*accesscontrol.Permission{ |
||||
{Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
for _, tt := range tests { |
||||
t.Run(tt.desc, func(t *testing.T) { |
||||
_, server, sql := setupTestEnvironment(t, &models.SignedInUser{OrgId: 1}, tt.permissions, testOptions) |
||||
|
||||
// seed team
|
||||
_, err := sql.CreateUser(context.Background(), models.CreateUserCommand{Login: "test", OrgId: 1}) |
||||
require.NoError(t, err) |
||||
|
||||
recorder := setPermission(t, server, testOptions.Resource, tt.resourceID, tt.permission, "users", strconv.Itoa(int(tt.userID))) |
||||
assert.Equal(t, tt.expectedStatus, recorder.Code) |
||||
|
||||
assert.Equal(t, tt.expectedStatus, recorder.Code) |
||||
if tt.expectedStatus == http.StatusOK { |
||||
permissions, _ := getPermission(t, server, testOptions.Resource, tt.resourceID) |
||||
require.Len(t, permissions, 1) |
||||
assert.Equal(t, tt.permission, permissions[0].Permission) |
||||
assert.Equal(t, tt.userID, permissions[0].UserID) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func setupTestEnvironment(t *testing.T, user *models.SignedInUser, permissions []*accesscontrol.Permission, ops Options) (*Service, *web.Mux, *sqlstore.SQLStore) { |
||||
sql := sqlstore.InitTestDB(t) |
||||
store := database.ProvideService(sql) |
||||
|
||||
service, err := New(ops, routing.NewRouteRegister(), accesscontrolmock.New().WithPermissions(permissions), store) |
||||
require.NoError(t, err) |
||||
|
||||
server := web.New() |
||||
server.UseMiddleware(web.Renderer(path.Join(setting.StaticRootPath, "views"), "[[", "]]")) |
||||
server.Use(contextProvider(&testContext{user})) |
||||
service.api.router.Register(server) |
||||
|
||||
return service, server, sql |
||||
} |
||||
|
||||
type testContext struct { |
||||
user *models.SignedInUser |
||||
} |
||||
|
||||
func contextProvider(tc *testContext) web.Handler { |
||||
return func(c *web.Context) { |
||||
signedIn := tc.user != nil |
||||
reqCtx := &models.ReqContext{ |
||||
Context: c, |
||||
SignedInUser: tc.user, |
||||
IsSignedIn: signedIn, |
||||
SkipCache: true, |
||||
Logger: log.New("test"), |
||||
} |
||||
c.Map(reqCtx) |
||||
} |
||||
} |
||||
|
||||
var testOptions = Options{ |
||||
Resource: "dashboards", |
||||
Assignments: Assignments{ |
||||
Users: true, |
||||
Teams: true, |
||||
BuiltInRoles: true, |
||||
}, |
||||
PermissionsToActions: map[string][]string{ |
||||
"View": {"dashboards:read"}, |
||||
"Edit": {"dashboards:read", "dashboards:write", "dashboards:delete"}, |
||||
}, |
||||
} |
||||
|
||||
func getPermission(t *testing.T, server *web.Mux, resource, resourceID string) ([]resourcePermissionDTO, *httptest.ResponseRecorder) { |
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/api/access-control/%s/%s", resource, resourceID), nil) |
||||
require.NoError(t, err) |
||||
recorder := httptest.NewRecorder() |
||||
server.ServeHTTP(recorder, req) |
||||
|
||||
var permissions []resourcePermissionDTO |
||||
if recorder.Code == http.StatusOK { |
||||
require.NoError(t, json.NewDecoder(recorder.Body).Decode(&permissions)) |
||||
} |
||||
return permissions, recorder |
||||
} |
||||
|
||||
func setPermission(t *testing.T, server *web.Mux, resource, resourceID, permission, assignment, assignTo string) *httptest.ResponseRecorder { |
||||
body := strings.NewReader(fmt.Sprintf(`{"permission": "%s"}`, permission)) |
||||
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("/api/access-control/%s/%s/%s/%s", resource, resourceID, assignment, assignTo), body) |
||||
require.NoError(t, err) |
||||
req.Header.Set("Content-Type", "application/json") |
||||
recorder := httptest.NewRecorder() |
||||
server.ServeHTTP(recorder, req) |
||||
|
||||
return recorder |
||||
} |
||||
@ -0,0 +1,8 @@ |
||||
package resourcepermissions |
||||
|
||||
import "errors" |
||||
|
||||
var ( |
||||
ErrInvalidActions = errors.New("invalid actions") |
||||
ErrInvalidAssignment = errors.New("invalid assignment") |
||||
) |
||||
@ -0,0 +1,27 @@ |
||||
package resourcepermissions |
||||
|
||||
import ( |
||||
"context" |
||||
) |
||||
|
||||
type ResourceValidator func(ctx context.Context, orgID int64, resourceID string) error |
||||
|
||||
type Options struct { |
||||
// Resource is the action and scope prefix that is generated
|
||||
Resource string |
||||
// ResourceValidator is a validator function that will be called before each assignment.
|
||||
// If set to nil the validator will be skipped
|
||||
ResourceValidator ResourceValidator |
||||
// Assignments decides what we can assign permissions to (users/teams/builtInRoles)
|
||||
Assignments Assignments |
||||
// PermissionsToAction is a map of friendly named permissions and what access control actions they should generate.
|
||||
// E.g. Edit permissions should generate dashboards:read, dashboards:write and dashboards:delete
|
||||
PermissionsToActions map[string][]string |
||||
|
||||
// ReaderRoleName is the display name for the generated fixed reader role
|
||||
ReaderRoleName string |
||||
// WriterRoleName is the display name for the generated fixed writer role
|
||||
WriterRoleName string |
||||
// RoleGroup is the group name for the generated fixed roles
|
||||
RoleGroup string |
||||
} |
||||
@ -0,0 +1,230 @@ |
||||
package resourcepermissions |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"sort" |
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore" |
||||
|
||||
"github.com/grafana/grafana/pkg/api/routing" |
||||
"github.com/grafana/grafana/pkg/models" |
||||
"github.com/grafana/grafana/pkg/services/accesscontrol" |
||||
) |
||||
|
||||
func New(options Options, router routing.RouteRegister, ac accesscontrol.AccessControl, store accesscontrol.ResourcePermissionsStore) (*Service, error) { |
||||
var permissions []string |
||||
validActions := make(map[string]struct{}) |
||||
for permission, actions := range options.PermissionsToActions { |
||||
permissions = append(permissions, permission) |
||||
for _, a := range actions { |
||||
validActions[a] = struct{}{} |
||||
} |
||||
} |
||||
|
||||
// Sort all permissions based on action length. Will be used when mapping between actions to permissions
|
||||
sort.Slice(permissions, func(i, j int) bool { |
||||
return len(options.PermissionsToActions[permissions[i]]) > len(options.PermissionsToActions[permissions[j]]) |
||||
}) |
||||
|
||||
actions := make([]string, 0, len(validActions)) |
||||
for action := range validActions { |
||||
actions = append(actions, action) |
||||
} |
||||
|
||||
s := &Service{ |
||||
ac: ac, |
||||
store: store, |
||||
options: options, |
||||
permissions: permissions, |
||||
actions: actions, |
||||
validActions: validActions, |
||||
} |
||||
|
||||
s.api = newApi(ac, router, s) |
||||
|
||||
if err := s.declareFixedRoles(); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
s.api.registerEndpoints() |
||||
|
||||
return s, nil |
||||
} |
||||
|
||||
// Service is used to create access control sub system including api / and service for managed resource permission
|
||||
type Service struct { |
||||
ac accesscontrol.AccessControl |
||||
store accesscontrol.ResourcePermissionsStore |
||||
api *api |
||||
|
||||
options Options |
||||
permissions []string |
||||
actions []string |
||||
validActions map[string]struct{} |
||||
} |
||||
|
||||
func (s *Service) GetPermissions(ctx context.Context, orgID int64, resourceID string) ([]accesscontrol.ResourcePermission, error) { |
||||
return s.store.GetResourcesPermissions(ctx, orgID, accesscontrol.GetResourcesPermissionsQuery{ |
||||
Actions: s.actions, |
||||
Resource: s.options.Resource, |
||||
ResourceIDs: []string{resourceID}, |
||||
}) |
||||
} |
||||
|
||||
func (s *Service) SetUserPermission(ctx context.Context, orgID, userID int64, resourceID string, actions []string) (*accesscontrol.ResourcePermission, error) { |
||||
if !s.options.Assignments.Users { |
||||
return nil, ErrInvalidAssignment |
||||
} |
||||
|
||||
if !s.validateActions(actions) { |
||||
return nil, ErrInvalidActions |
||||
} |
||||
|
||||
if err := s.validateResource(ctx, orgID, resourceID); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if err := s.validateUser(ctx, orgID, userID); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return s.store.SetUserResourcePermission(ctx, orgID, userID, accesscontrol.SetResourcePermissionCommand{ |
||||
Actions: actions, |
||||
ResourceID: resourceID, |
||||
Resource: s.options.Resource, |
||||
}) |
||||
} |
||||
|
||||
func (s *Service) SetTeamPermission(ctx context.Context, orgID, teamID int64, resourceID string, actions []string) (*accesscontrol.ResourcePermission, error) { |
||||
if !s.options.Assignments.Teams { |
||||
return nil, ErrInvalidAssignment |
||||
} |
||||
if !s.validateActions(actions) { |
||||
return nil, ErrInvalidActions |
||||
} |
||||
|
||||
if err := s.validateTeam(ctx, orgID, teamID); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if err := s.validateResource(ctx, orgID, resourceID); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return s.store.SetTeamResourcePermission(ctx, orgID, teamID, accesscontrol.SetResourcePermissionCommand{ |
||||
Actions: actions, |
||||
ResourceID: resourceID, |
||||
Resource: s.options.Resource, |
||||
}) |
||||
} |
||||
|
||||
func (s *Service) SetBuiltInRolePermission(ctx context.Context, orgID int64, builtInRole string, resourceID string, actions []string) (*accesscontrol.ResourcePermission, error) { |
||||
if !s.options.Assignments.BuiltInRoles { |
||||
return nil, ErrInvalidAssignment |
||||
} |
||||
|
||||
if !s.validateActions(actions) { |
||||
return nil, ErrInvalidActions |
||||
} |
||||
|
||||
if err := s.validateBuiltinRole(ctx, builtInRole); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if err := s.validateResource(ctx, orgID, resourceID); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return s.store.SetBuiltInResourcePermission(ctx, orgID, builtInRole, accesscontrol.SetResourcePermissionCommand{ |
||||
Actions: actions, |
||||
ResourceID: resourceID, |
||||
Resource: s.options.Resource, |
||||
}) |
||||
} |
||||
|
||||
func (s *Service) MapActions(permission accesscontrol.ResourcePermission) string { |
||||
for _, p := range s.permissions { |
||||
if permission.Contains(s.options.PermissionsToActions[p]) { |
||||
return p |
||||
} |
||||
} |
||||
return "" |
||||
} |
||||
|
||||
func (s *Service) MapPermission(permission string) []string { |
||||
for k, v := range s.options.PermissionsToActions { |
||||
if permission == k { |
||||
return v |
||||
} |
||||
} |
||||
return []string{} |
||||
} |
||||
|
||||
func (s *Service) validateResource(ctx context.Context, orgID int64, resourceID string) error { |
||||
if s.options.ResourceValidator != nil { |
||||
return s.options.ResourceValidator(ctx, orgID, resourceID) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (s *Service) validateActions(actions []string) bool { |
||||
for _, a := range actions { |
||||
if _, ok := s.validActions[a]; !ok { |
||||
return false |
||||
} |
||||
} |
||||
return true |
||||
} |
||||
|
||||
func (s *Service) validateUser(ctx context.Context, orgID, userID int64) error { |
||||
if err := sqlstore.GetSignedInUser(ctx, &models.GetSignedInUserQuery{OrgId: orgID, UserId: userID}); err != nil { |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (s *Service) validateTeam(ctx context.Context, orgID, teamID int64) error { |
||||
if err := sqlstore.GetTeamById(ctx, &models.GetTeamByIdQuery{OrgId: orgID, Id: teamID}); err != nil { |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (s *Service) validateBuiltinRole(ctx context.Context, builtinRole string) error { |
||||
if err := accesscontrol.ValidateBuiltInRoles([]string{builtinRole}); err != nil { |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (s *Service) declareFixedRoles() error { |
||||
scopeAll := accesscontrol.Scope(s.options.Resource, "*") |
||||
readerRole := accesscontrol.RoleRegistration{ |
||||
Role: accesscontrol.RoleDTO{ |
||||
Version: 5, |
||||
Name: fmt.Sprintf("fixed:%s.permissions:reader", s.options.Resource), |
||||
DisplayName: s.options.ReaderRoleName, |
||||
Group: s.options.RoleGroup, |
||||
Permissions: []accesscontrol.Permission{ |
||||
{Action: fmt.Sprintf("%s.permissions:read", s.options.Resource), Scope: scopeAll}, |
||||
}, |
||||
}, |
||||
Grants: []string{string(models.ROLE_ADMIN)}, |
||||
} |
||||
|
||||
writerRole := accesscontrol.RoleRegistration{ |
||||
Role: accesscontrol.RoleDTO{ |
||||
Version: 5, |
||||
Name: fmt.Sprintf("fixed:%s.permissions:writer", s.options.Resource), |
||||
DisplayName: s.options.WriterRoleName, |
||||
Group: s.options.RoleGroup, |
||||
Permissions: accesscontrol.ConcatPermissions(readerRole.Role.Permissions, []accesscontrol.Permission{ |
||||
{Action: fmt.Sprintf("%s.permissions:write", s.options.Resource), Scope: scopeAll}, |
||||
}), |
||||
}, |
||||
Grants: []string{string(models.ROLE_ADMIN)}, |
||||
} |
||||
|
||||
return s.ac.DeclareFixedRoles(readerRole, writerRole) |
||||
} |
||||
@ -0,0 +1,101 @@ |
||||
import React, { useEffect, useMemo, useState } from 'react'; |
||||
import { UserPicker } from 'app/core/components/Select/UserPicker'; |
||||
import { TeamPicker } from 'app/core/components/Select/TeamPicker'; |
||||
import { Button, Form, HorizontalGroup, Select } from '@grafana/ui'; |
||||
import { OrgRole } from 'app/types/acl'; |
||||
import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; |
||||
import { Assignments, PermissionTarget, SetPermission } from './types'; |
||||
|
||||
export interface Props { |
||||
permissions: string[]; |
||||
assignments: Assignments; |
||||
canListUsers: boolean; |
||||
onCancel: () => void; |
||||
onAdd: (state: SetPermission) => void; |
||||
} |
||||
|
||||
export const AddPermission = ({ permissions, assignments, canListUsers, onAdd, onCancel }: Props) => { |
||||
const [target, setPermissionTarget] = useState<PermissionTarget>(PermissionTarget.User); |
||||
const [teamId, setTeamId] = useState(0); |
||||
const [userId, setUserId] = useState(0); |
||||
const [builtInRole, setBuiltinRole] = useState(''); |
||||
const [permission, setPermission] = useState(''); |
||||
|
||||
const targetOptions = useMemo(() => { |
||||
const options = []; |
||||
if (assignments.users && canListUsers) { |
||||
options.push({ value: PermissionTarget.User, label: 'User', isDisabled: false }); |
||||
} |
||||
if (assignments.teams) { |
||||
options.push({ value: PermissionTarget.Team, label: 'Team' }); |
||||
} |
||||
if (assignments.builtInRoles) { |
||||
options.push({ value: PermissionTarget.BuiltInRole, label: 'Role' }); |
||||
} |
||||
return options; |
||||
}, [assignments, canListUsers]); |
||||
|
||||
useEffect(() => { |
||||
if (permissions.length > 0) { |
||||
setPermission(permissions[0]); |
||||
} |
||||
}, [permissions]); |
||||
|
||||
const isValid = () => |
||||
(target === PermissionTarget.Team && teamId > 0) || |
||||
(target === PermissionTarget.User && userId > 0) || |
||||
(PermissionTarget.BuiltInRole && OrgRole.hasOwnProperty(builtInRole)); |
||||
|
||||
return ( |
||||
<div className="cta-form" aria-label="Permissions slider"> |
||||
<CloseButton onClick={onCancel} /> |
||||
<h5>Add Permission For</h5> |
||||
<Form |
||||
name="addPermission" |
||||
maxWidth="none" |
||||
onSubmit={() => onAdd({ userId, teamId, builtInRole, permission, target })} |
||||
> |
||||
{() => ( |
||||
<HorizontalGroup> |
||||
<Select |
||||
aria-label="Role to add new permission to" |
||||
value={target} |
||||
options={targetOptions} |
||||
onChange={(v) => setPermissionTarget(v.value!)} |
||||
menuShouldPortal |
||||
/> |
||||
|
||||
{target === PermissionTarget.User && ( |
||||
<UserPicker onSelected={(u) => setUserId(u.value || 0)} className={'width-20'} /> |
||||
)} |
||||
|
||||
{target === PermissionTarget.Team && ( |
||||
<TeamPicker onSelected={(t) => setTeamId(t.value?.id || 0)} className={'width-20'} /> |
||||
)} |
||||
|
||||
{target === PermissionTarget.BuiltInRole && ( |
||||
<Select |
||||
aria-label={'Built-in role picker'} |
||||
menuShouldPortal |
||||
options={Object.values(OrgRole).map((r) => ({ value: r, label: r }))} |
||||
onChange={(r) => setBuiltinRole(r.value || '')} |
||||
width={40} |
||||
/> |
||||
)} |
||||
|
||||
<Select |
||||
width={25} |
||||
menuShouldPortal |
||||
value={permissions.find((p) => p === permission)} |
||||
options={permissions.map((p) => ({ label: p, value: p }))} |
||||
onChange={(v) => setPermission(v.value || '')} |
||||
/> |
||||
<Button type="submit" disabled={!isValid()}> |
||||
Save |
||||
</Button> |
||||
</HorizontalGroup> |
||||
)} |
||||
</Form> |
||||
</div> |
||||
); |
||||
}; |
||||
@ -0,0 +1,38 @@ |
||||
import React from 'react'; |
||||
import { ResourcePermission } from './types'; |
||||
import { PermissionListItem } from './PermissionListItem'; |
||||
|
||||
interface Props { |
||||
title: string; |
||||
items: ResourcePermission[]; |
||||
permissionLevels: string[]; |
||||
canRemove: boolean; |
||||
onRemove: (item: ResourcePermission) => void; |
||||
onChange: (resourcePermission: ResourcePermission, permission: string) => void; |
||||
} |
||||
|
||||
export const PermissionList = ({ title, items, permissionLevels, canRemove, onRemove, onChange }: Props) => { |
||||
if (items.length === 0) { |
||||
return null; |
||||
} |
||||
|
||||
return ( |
||||
<div> |
||||
<h5>{title}</h5> |
||||
<table className="filter-table gf-form-group"> |
||||
<tbody> |
||||
{items.map((item, index) => ( |
||||
<PermissionListItem |
||||
item={item} |
||||
onRemove={onRemove} |
||||
onChange={onChange} |
||||
canRemove={canRemove} |
||||
key={`${index}-${item.userId}`} |
||||
permissionLevels={permissionLevels} |
||||
/> |
||||
))} |
||||
</tbody> |
||||
</table> |
||||
</div> |
||||
); |
||||
}; |
||||
@ -0,0 +1,84 @@ |
||||
import React from 'react'; |
||||
import { ResourcePermission } from './types'; |
||||
import { Button, Icon, Select, Tooltip } from '@grafana/ui'; |
||||
|
||||
interface Props { |
||||
item: ResourcePermission; |
||||
permissionLevels: string[]; |
||||
canRemove: boolean; |
||||
onRemove: (item: ResourcePermission) => void; |
||||
onChange: (item: ResourcePermission, permission: string) => void; |
||||
} |
||||
|
||||
export const PermissionListItem = ({ item, permissionLevels, canRemove, onRemove, onChange }: Props) => ( |
||||
<tr> |
||||
<td style={{ width: '1%' }}>{getAvatar(item)}</td> |
||||
<td style={{ width: '90%' }}>{getDescription(item)}</td> |
||||
<td /> |
||||
<td className="query-keyword">Can</td> |
||||
<td> |
||||
<div className="gf-form"> |
||||
<Select |
||||
className="width-20" |
||||
menuShouldPortal |
||||
onChange={(p) => onChange(item, p.value!)} |
||||
value={permissionLevels.find((p) => p === item.permission)} |
||||
options={permissionLevels.map((p) => ({ value: p, label: p }))} |
||||
/> |
||||
</div> |
||||
</td> |
||||
<td> |
||||
<Tooltip content={getPermissionInfo(item)}> |
||||
<Icon name="info-circle" /> |
||||
</Tooltip> |
||||
</td> |
||||
<td> |
||||
{item.isManaged ? ( |
||||
<Button |
||||
size="sm" |
||||
icon="times" |
||||
variant="destructive" |
||||
disabled={!canRemove} |
||||
onClick={() => onRemove(item)} |
||||
aria-label={`Remove permission for ${getName(item)}`} |
||||
/> |
||||
) : ( |
||||
<Tooltip content="Provisioned permission"> |
||||
<Button size="sm" icon="lock" /> |
||||
</Tooltip> |
||||
)} |
||||
</td> |
||||
</tr> |
||||
); |
||||
|
||||
const getAvatar = (item: ResourcePermission) => { |
||||
if (item.teamId) { |
||||
return <img className="filter-table__avatar" src={item.teamAvatarUrl} alt={`Avatar for team ${item.teamId}`} />; |
||||
} else if (item.userId) { |
||||
return <img className="filter-table__avatar" src={item.userAvatarUrl} alt={`Avatar for user ${item.userId}`} />; |
||||
} |
||||
return <Icon size="xl" name="shield" />; |
||||
}; |
||||
|
||||
const getName = (item: ResourcePermission) => { |
||||
if (item.userId) { |
||||
return item.userLogin; |
||||
} |
||||
if (item.teamId) { |
||||
return item.team; |
||||
} |
||||
return item.builtInRole; |
||||
}; |
||||
|
||||
const getDescription = (item: ResourcePermission) => { |
||||
if (item.userId) { |
||||
return <span key="name">{item.userLogin} </span>; |
||||
} else if (item.teamId) { |
||||
return <span key="name">{item.team} </span>; |
||||
} else if (item.builtInRole) { |
||||
return <span key="name">{item.builtInRole} </span>; |
||||
} |
||||
return <span key="name" />; |
||||
}; |
||||
|
||||
const getPermissionInfo = (p: ResourcePermission) => `Actions: ${[...new Set(p.actions)].sort().join(' ')}`; |
||||
@ -0,0 +1,193 @@ |
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'; |
||||
import { sortBy } from 'lodash'; |
||||
import { getBackendSrv } from 'app/core/services/backend_srv'; |
||||
|
||||
import { Button } from '@grafana/ui'; |
||||
import { SlideDown } from 'app/core/components/Animations/SlideDown'; |
||||
import { AddPermission } from './AddPermission'; |
||||
import { PermissionList } from './PermissionList'; |
||||
import { PermissionTarget, ResourcePermission, SetPermission, Description } from './types'; |
||||
|
||||
const EMPTY_PERMISSION = ''; |
||||
|
||||
const INITIAL_DESCRIPTION: Description = { |
||||
permissions: [], |
||||
assignments: { |
||||
teams: false, |
||||
users: false, |
||||
builtInRoles: false, |
||||
}, |
||||
}; |
||||
|
||||
export type Props = { |
||||
resource: string; |
||||
resourceId: number; |
||||
|
||||
canListUsers: boolean; |
||||
canSetPermissions: boolean; |
||||
}; |
||||
|
||||
export const Permissions = ({ resource, resourceId, canListUsers, canSetPermissions }: Props) => { |
||||
const [isAdding, setIsAdding] = useState(false); |
||||
const [items, setItems] = useState<ResourcePermission[]>([]); |
||||
const [desc, setDesc] = useState(INITIAL_DESCRIPTION); |
||||
|
||||
const fetchItems = useCallback(() => { |
||||
return getPermissions(resource, resourceId).then((r) => setItems(r)); |
||||
}, [resource, resourceId]); |
||||
|
||||
useEffect(() => { |
||||
getDescription(resource).then((r) => { |
||||
setDesc(r); |
||||
return fetchItems(); |
||||
}); |
||||
}, [resource, resourceId, fetchItems]); |
||||
|
||||
const onAdd = (state: SetPermission) => { |
||||
let promise: Promise<void> | null = null; |
||||
if (state.target === PermissionTarget.User) { |
||||
promise = setUserPermission(resource, resourceId, state.userId!, state.permission); |
||||
} else if (state.target === PermissionTarget.Team) { |
||||
promise = setTeamPermission(resource, resourceId, state.teamId!, state.permission); |
||||
} else if (state.target === PermissionTarget.BuiltInRole) { |
||||
promise = setBuiltInRolePermission(resource, resourceId, state.builtInRole!, state.permission); |
||||
} |
||||
|
||||
if (promise !== null) { |
||||
promise.then(fetchItems); |
||||
} |
||||
}; |
||||
|
||||
const onRemove = (item: ResourcePermission) => { |
||||
let promise: Promise<void> | null = null; |
||||
if (item.userId) { |
||||
promise = setUserPermission(resource, resourceId, item.userId, EMPTY_PERMISSION); |
||||
} else if (item.teamId) { |
||||
promise = setTeamPermission(resource, resourceId, item.teamId, EMPTY_PERMISSION); |
||||
} else if (item.builtInRole) { |
||||
promise = setBuiltInRolePermission(resource, resourceId, item.builtInRole, EMPTY_PERMISSION); |
||||
} |
||||
|
||||
if (promise !== null) { |
||||
promise.then(fetchItems); |
||||
} |
||||
}; |
||||
|
||||
const onChange = (item: ResourcePermission, permission: string) => { |
||||
if (item.permission === permission) { |
||||
return; |
||||
} |
||||
if (item.userId) { |
||||
onAdd({ permission, userId: item.userId, target: PermissionTarget.User }); |
||||
} else if (item.teamId) { |
||||
onAdd({ permission, teamId: item.teamId, target: PermissionTarget.Team }); |
||||
} else if (item.builtInRole) { |
||||
onAdd({ permission, builtInRole: item.builtInRole, target: PermissionTarget.BuiltInRole }); |
||||
} |
||||
}; |
||||
|
||||
const teams = useMemo( |
||||
() => |
||||
sortBy( |
||||
items.filter((i) => i.teamId), |
||||
['team'] |
||||
), |
||||
[items] |
||||
); |
||||
const users = useMemo( |
||||
() => |
||||
sortBy( |
||||
items.filter((i) => i.userId), |
||||
['userLogin'] |
||||
), |
||||
[items] |
||||
); |
||||
const builtInRoles = useMemo( |
||||
() => |
||||
sortBy( |
||||
items.filter((i) => i.builtInRole), |
||||
['builtInRole'] |
||||
), |
||||
[items] |
||||
); |
||||
|
||||
return ( |
||||
<div> |
||||
<div className="page-action-bar"> |
||||
<h3 className="page-sub-heading">Permissions</h3> |
||||
<div className="page-action-bar__spacer" /> |
||||
{canSetPermissions && ( |
||||
<Button variant={'primary'} key="add-permission" onClick={() => setIsAdding(true)}> |
||||
Add a permission |
||||
</Button> |
||||
)} |
||||
</div> |
||||
|
||||
<div> |
||||
<SlideDown in={isAdding}> |
||||
<AddPermission |
||||
onAdd={onAdd} |
||||
permissions={desc.permissions} |
||||
assignments={desc.assignments} |
||||
canListUsers={canListUsers} |
||||
onCancel={() => setIsAdding(false)} |
||||
/> |
||||
</SlideDown> |
||||
<PermissionList |
||||
title="Roles" |
||||
items={builtInRoles} |
||||
permissionLevels={desc.permissions} |
||||
onChange={onChange} |
||||
onRemove={onRemove} |
||||
canRemove={canSetPermissions} |
||||
/> |
||||
<PermissionList |
||||
title="Users" |
||||
items={users} |
||||
permissionLevels={desc.permissions} |
||||
onChange={onChange} |
||||
onRemove={onRemove} |
||||
canRemove={canSetPermissions} |
||||
/> |
||||
<PermissionList |
||||
title="Teams" |
||||
items={teams} |
||||
permissionLevels={desc.permissions} |
||||
onChange={onChange} |
||||
onRemove={onRemove} |
||||
canRemove={canSetPermissions} |
||||
/> |
||||
</div> |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
const getDescription = async (resource: string): Promise<Description> => { |
||||
try { |
||||
return await getBackendSrv().get(`/api/access-control/${resource}/description`); |
||||
} catch (e) { |
||||
console.error('failed to load resource description: ', e); |
||||
return INITIAL_DESCRIPTION; |
||||
} |
||||
}; |
||||
|
||||
const getPermissions = (resource: string, datasourceId: number): Promise<ResourcePermission[]> => |
||||
getBackendSrv().get(`/api/access-control/${resource}/${datasourceId}`); |
||||
|
||||
const setUserPermission = (resource: string, resourceId: number, userId: number, permission: string) => |
||||
setPermission(resource, resourceId, 'users', userId, permission); |
||||
|
||||
const setTeamPermission = (resource: string, resourceId: number, teamId: number, permission: string) => |
||||
setPermission(resource, resourceId, 'teams', teamId, permission); |
||||
|
||||
const setBuiltInRolePermission = (resource: string, resourceId: number, builtInRole: string, permission: string) => |
||||
setPermission(resource, resourceId, 'builtInRoles', builtInRole, permission); |
||||
|
||||
const setPermission = ( |
||||
resource: string, |
||||
resourceId: number, |
||||
type: 'users' | 'teams' | 'builtInRoles', |
||||
typeId: number | string, |
||||
permission: string |
||||
): Promise<void> => |
||||
getBackendSrv().post(`/api/access-control/${resource}/${resourceId}/${type}/${typeId}/`, { permission }); |
||||
@ -0,0 +1 @@ |
||||
export * from './Permissions'; |
||||
@ -0,0 +1,38 @@ |
||||
export type ResourcePermission = { |
||||
id: number; |
||||
resourceId: string; |
||||
isManaged: boolean; |
||||
userId?: number; |
||||
userLogin?: string; |
||||
userAvatarUrl?: string; |
||||
team?: string; |
||||
teamId?: number; |
||||
teamAvatarUrl?: string; |
||||
builtInRole?: string; |
||||
actions: string[]; |
||||
permission: string; |
||||
}; |
||||
|
||||
export type SetPermission = { |
||||
userId?: number; |
||||
teamId?: number; |
||||
builtInRole?: string; |
||||
permission: string; |
||||
target: PermissionTarget; |
||||
}; |
||||
|
||||
export enum PermissionTarget { |
||||
Team = 'Team', |
||||
User = 'User', |
||||
BuiltInRole = 'builtInRole', |
||||
} |
||||
export type Description = { |
||||
assignments: Assignments; |
||||
permissions: string[]; |
||||
}; |
||||
|
||||
export type Assignments = { |
||||
users: boolean; |
||||
teams: boolean; |
||||
builtInRoles: boolean; |
||||
}; |
||||
Loading…
Reference in new issue