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