Access control: Display inherited folder permissions in dashboards (#46421)

This commit is contained in:
Karl Persson 2022-03-17 17:08:51 +01:00 committed by GitHub
parent fb17b9f545
commit 4df7bf5ab2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 142 additions and 129 deletions

View File

@ -13,24 +13,30 @@ import (
)
type flatResourcePermission struct {
ID int64 `xorm:"id"`
ResourceID string `xorm:"resource_id"`
RoleName string
Action string
Scope string
UserId int64
UserLogin string
UserEmail string
TeamId int64
TeamEmail string
Team string
BuiltInRole string
Created time.Time
Updated time.Time
ID int64 `xorm:"id"`
RoleName string
Action string
// Scope is what is stored in the database
Scope string
// ResourceScope is what we ask for
ResourceScope string
UserId int64
UserLogin string
UserEmail string
TeamId int64
TeamEmail string
Team string
BuiltInRole string
Created time.Time
Updated time.Time
}
func (p *flatResourcePermission) Managed() bool {
return strings.HasPrefix(p.RoleName, "managed:")
func (p *flatResourcePermission) IsManaged() bool {
return strings.HasPrefix(p.RoleName, "managed:") && !p.IsInherited()
}
func (p *flatResourcePermission) IsInherited() bool {
return p.Scope != p.ResourceScope
}
func (s *AccessControlStore) SetUserResourcePermission(
@ -236,7 +242,7 @@ func (s *AccessControlStore) setResourcePermission(
keep = append(keep, id)
}
permissions, err := s.getResourcePermissions(sess, cmd.ResourceID, keep)
permissions, err := s.getResourcePermissionsByIds(sess, cmd.Resource, cmd.ResourceID, keep)
if err != nil {
return nil, err
}
@ -249,12 +255,12 @@ func (s *AccessControlStore) setResourcePermission(
return permission, nil
}
func (s *AccessControlStore) GetResourcesPermissions(ctx context.Context, orgID int64, query types.GetResourcesPermissionsQuery) ([]accesscontrol.ResourcePermission, error) {
func (s *AccessControlStore) GetResourcePermissions(ctx context.Context, orgID int64, query types.GetResourcePermissionsQuery) ([]accesscontrol.ResourcePermission, error) {
var result []accesscontrol.ResourcePermission
err := s.sql.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
var err error
result, err = s.getResourcesPermissions(sess, orgID, query)
result, err = s.getResourcePermissions(sess, orgID, query)
return err
})
@ -273,19 +279,16 @@ func (s *AccessControlStore) createResourcePermission(sess *sqlstore.DBSession,
return permission.ID, nil
}
func (s *AccessControlStore) getResourcesPermissions(sess *sqlstore.DBSession, orgID int64, query types.GetResourcesPermissionsQuery) ([]accesscontrol.ResourcePermission, error) {
func (s *AccessControlStore) getResourcePermissions(sess *sqlstore.DBSession, orgID int64, query types.GetResourcePermissionsQuery) ([]accesscontrol.ResourcePermission, error) {
if len(query.Actions) == 0 {
return nil, nil
}
if len(query.ResourceIDs) == 0 {
return nil, nil
}
rawSelect := `
SELECT
p.*,
r.name as role_name,
? as resource_scope,
`
userSelect := rawSelect + `
@ -334,25 +337,31 @@ func (s *AccessControlStore) getResourcesPermissions(sess *sqlstore.DBSession, o
builtinFrom := rawFrom + `
INNER JOIN builtin_role br ON r.id = br.role_id AND (br.org_id = 0 OR br.org_id = ?)
`
where := `
WHERE (r.org_id = ? OR r.org_id = 0)
AND (p.scope = '*' OR p.scope = ? OR p.scope = ? OR p.scope IN (?` + strings.Repeat(",?", len(query.ResourceIDs)-1) + `))
AND p.action IN (?` + strings.Repeat(",?", len(query.Actions)-1) + `)
`
if query.OnlyManaged {
where += `AND r.name LIKE 'managed:%'`
}
where := `WHERE (r.org_id = ? OR r.org_id = 0) AND (p.scope = '*' OR p.scope = ? OR p.scope = ? OR p.scope = ?`
scope := accesscontrol.GetResourceScope(query.Resource, query.ResourceID)
args := []interface{}{
scope,
orgID,
orgID,
accesscontrol.GetResourceAllScope(query.Resource),
accesscontrol.GetResourceAllIDScope(query.Resource),
scope,
}
for _, id := range query.ResourceIDs {
args = append(args, accesscontrol.GetResourceScope(query.Resource, id))
if len(query.InheritedScopes) > 0 {
where += ` OR p.scope IN(?` + strings.Repeat(",?", len(query.InheritedScopes)-1) + `)`
for _, scope := range query.InheritedScopes {
args = append(args, scope)
}
}
where += `) AND p.action IN (?` + strings.Repeat(",?", len(query.Actions)-1) + `)`
if query.OnlyManaged {
where += `AND r.name LIKE 'managed:%'`
}
for _, a := range query.Actions {
@ -386,33 +395,16 @@ func (s *AccessControlStore) getResourcesPermissions(sess *sqlstore.DBSession, o
return nil, err
}
scopeAll := accesscontrol.GetResourceAllScope(query.Resource)
scopeAllIDs := accesscontrol.GetResourceAllIDScope(query.Resource)
byResource := make(map[string][]flatResourcePermission)
// Add resourceIds and generate permissions for `*`, `resource:*` and `resource:id:*`
for _, id := range query.ResourceIDs {
scope := accesscontrol.GetResourceScope(query.Resource, id)
for _, p := range queryResults {
if p.Scope == scope || p.Scope == scopeAll || p.Scope == scopeAllIDs || p.Scope == "*" {
p.ResourceID = id
byResource[p.ResourceID] = append(byResource[p.ResourceID], p)
}
}
}
var result []accesscontrol.ResourcePermission
for _, permissions := range byResource {
users, teams, builtins := groupPermissionsByAssignment(permissions)
for _, p := range users {
result = append(result, flatPermissionsToResourcePermissions(p)...)
}
for _, p := range teams {
result = append(result, flatPermissionsToResourcePermissions(p)...)
}
for _, p := range builtins {
result = append(result, flatPermissionsToResourcePermissions(p)...)
}
users, teams, builtins := groupPermissionsByAssignment(queryResults)
for _, p := range users {
result = append(result, flatPermissionsToResourcePermissions(p)...)
}
for _, p := range teams {
result = append(result, flatPermissionsToResourcePermissions(p)...)
}
for _, p := range builtins {
result = append(result, flatPermissionsToResourcePermissions(p)...)
}
return result, nil
@ -439,7 +431,7 @@ func groupPermissionsByAssignment(permissions []flatResourcePermission) (map[int
func flatPermissionsToResourcePermissions(permissions []flatResourcePermission) []accesscontrol.ResourcePermission {
var managed, provisioned []flatResourcePermission
for _, p := range permissions {
if p.Managed() {
if p.IsManaged() {
managed = append(managed, p)
} else {
provisioned = append(provisioned, p)
@ -470,7 +462,6 @@ func flatPermissionsToResourcePermission(permissions []flatResourcePermission) *
first := permissions[0]
return &accesscontrol.ResourcePermission{
ID: first.ID,
ResourceID: first.ResourceID,
RoleName: first.RoleName,
Actions: actions,
Scope: first.Scope,
@ -483,6 +474,7 @@ func flatPermissionsToResourcePermission(permissions []flatResourcePermission) *
BuiltInRole: first.BuiltInRole,
Created: first.Created,
Updated: first.Updated,
IsManaged: first.IsManaged(),
}
}
@ -582,7 +574,7 @@ func (s *AccessControlStore) getOrCreateManagedRole(sess *sqlstore.DBSession, or
return &role, nil
}
func (s *AccessControlStore) getResourcePermissions(sess *sqlstore.DBSession, resourceID string, ids []int64) ([]flatResourcePermission, error) {
func (s *AccessControlStore) getResourcePermissionsByIds(sess *sqlstore.DBSession, resource, resourceID string, ids []int64) ([]flatResourcePermission, error) {
var result []flatResourcePermission
if len(ids) == 0 {
return result, nil
@ -590,7 +582,7 @@ func (s *AccessControlStore) getResourcePermissions(sess *sqlstore.DBSession, re
rawSql := `
SELECT
p.*,
? AS resource_id,
? as resource_scope,
ur.user_id AS user_id,
u.login AS user_login,
u.email AS user_email,
@ -610,7 +602,7 @@ func (s *AccessControlStore) getResourcePermissions(sess *sqlstore.DBSession, re
`
args := make([]interface{}, 0, len(ids)+1)
args = append(args, resourceID)
args = append(args, accesscontrol.GetResourceScope(resource, resourceID))
for _, id := range ids {
args = append(args, id)
}

View File

@ -55,10 +55,10 @@ func benchmarkDSPermissions(b *testing.B, dsNum, usersNum int) {
func getDSPermissions(b *testing.B, store *AccessControlStore, dataSources []int64) {
dsId := dataSources[0]
permissions, err := store.GetResourcesPermissions(context.Background(), accesscontrol.GlobalOrgID, types.GetResourcesPermissionsQuery{
Actions: []string{dsAction},
Resource: dsResource,
ResourceIDs: []string{strconv.Itoa(int(dsId))},
permissions, err := store.GetResourcePermissions(context.Background(), accesscontrol.GlobalOrgID, types.GetResourcePermissionsQuery{
Actions: []string{dsAction},
Resource: dsResource,
ResourceID: strconv.Itoa(int(dsId)),
})
require.NoError(b, err)
assert.GreaterOrEqual(b, len(permissions), 2)

View File

@ -311,29 +311,29 @@ func TestAccessControlStore_SetResourcePermissions(t *testing.T) {
}
}
type getResourcesPermissionsTest struct {
type getResourcePermissionsTest struct {
desc string
user *models.SignedInUser
numUsers int
actions []string
resource string
resourceIDs []string
resourceID string
onlyManaged bool
}
func TestAccessControlStore_GetResourcesPermissions(t *testing.T) {
tests := []getResourcesPermissionsTest{
func TestAccessControlStore_GetResourcePermissions(t *testing.T) {
tests := []getResourcePermissionsTest{
{
desc: "should return permissions for all resource ids",
desc: "should return permissions for resource id",
user: &models.SignedInUser{
OrgId: 1,
Permissions: map[int64]map[string][]string{
1: {accesscontrol.ActionOrgUsersRead: {accesscontrol.ScopeUsersAll}},
}},
numUsers: 3,
actions: []string{"datasources:query"},
resource: "datasources",
resourceIDs: []string{"1", "2"},
numUsers: 3,
actions: []string{"datasources:query"},
resource: "datasources",
resourceID: "1",
},
{
desc: "should return manage permissions for all resource ids",
@ -345,7 +345,7 @@ func TestAccessControlStore_GetResourcesPermissions(t *testing.T) {
numUsers: 3,
actions: []string{"datasources:query"},
resource: "datasources",
resourceIDs: []string{"1", "2"},
resourceID: "1",
onlyManaged: true,
},
}
@ -389,22 +389,20 @@ func TestAccessControlStore_GetResourcesPermissions(t *testing.T) {
})
require.NoError(t, err)
for _, id := range test.resourceIDs {
seedResourcePermissions(t, store, sql, test.actions, test.resource, id, test.numUsers)
}
seedResourcePermissions(t, store, sql, test.actions, test.resource, test.resourceID, test.numUsers)
permissions, err := store.GetResourcesPermissions(context.Background(), test.user.OrgId, types.GetResourcesPermissionsQuery{
permissions, err := store.GetResourcePermissions(context.Background(), test.user.OrgId, types.GetResourcePermissionsQuery{
User: test.user,
Actions: test.actions,
Resource: test.resource,
ResourceIDs: test.resourceIDs,
ResourceID: test.resourceID,
OnlyManaged: test.onlyManaged,
})
require.NoError(t, err)
expectedLen := test.numUsers * len(test.resourceIDs)
expectedLen := test.numUsers
if !test.onlyManaged {
expectedLen += len(test.resourceIDs)
expectedLen += 1
}
assert.Len(t, permissions, expectedLen)
})

View File

@ -195,7 +195,6 @@ type ScopeParams struct {
// can perform against specific resource.
type ResourcePermission struct {
ID int64
ResourceID string
RoleName string
Actions []string
Scope string
@ -206,14 +205,11 @@ type ResourcePermission struct {
TeamEmail string
Team string
BuiltInRole string
IsManaged bool
Created time.Time
Updated time.Time
}
func (p *ResourcePermission) IsManaged() bool {
return strings.HasPrefix(p.RoleName, "managed:")
}
func (p *ResourcePermission) Contains(targetActions []string) bool {
if len(p.Actions) < len(targetActions) {
return false

View File

@ -146,21 +146,29 @@ var FolderAdminActions = append(FolderEditActions, []string{dashboards.ActionFol
func provideDashboardService(
cfg *setting.Cfg, router routing.RouteRegister, sql *sqlstore.SQLStore,
accesscontrol accesscontrol.AccessControl, store resourcepermissions.Store,
ac accesscontrol.AccessControl, store resourcepermissions.Store,
) (*resourcepermissions.Service, error) {
getDashboard := func(ctx context.Context, orgID int64, resourceID string) (*models.Dashboard, error) {
id, err := strconv.ParseInt(resourceID, 10, 64)
if err != nil {
return nil, err
}
query := &models.GetDashboardQuery{Id: id, OrgId: orgID}
if err := sql.GetDashboard(ctx, query); err != nil {
return nil, err
}
return query.Result, nil
}
options := resourcepermissions.Options{
Resource: "dashboards",
ResourceValidator: func(ctx context.Context, orgID int64, resourceID string) error {
id, err := strconv.ParseInt(resourceID, 10, 64)
dashboard, err := getDashboard(ctx, orgID, resourceID)
if err != nil {
return err
}
query := &models.GetDashboardQuery{Id: id, OrgId: orgID}
if err := sql.GetDashboard(ctx, query); err != nil {
return err
}
if query.Result.IsFolder {
if dashboard.IsFolder {
return errors.New("not found")
}
@ -176,6 +184,16 @@ func provideDashboardService(
}
return query.Result.Id, nil
},
InheritedScopesSolver: func(ctx context.Context, orgID int64, resourceID string) ([]string, error) {
dashboard, err := getDashboard(ctx, orgID, resourceID)
if err != nil {
return nil, err
}
if dashboard.FolderId > 0 {
return []string{accesscontrol.GetResourceScope("folders", strconv.FormatInt(dashboard.FolderId, 10))}, nil
}
return []string{}, nil
},
Assignments: resourcepermissions.Assignments{
Users: true,
Teams: true,
@ -191,7 +209,7 @@ func provideDashboardService(
RoleGroup: "Dashboards",
}
return resourcepermissions.New(options, cfg, router, accesscontrol, store, sql)
return resourcepermissions.New(options, cfg, router, ac, store, sql)
}
func provideFolderService(

View File

@ -65,7 +65,6 @@ func (a *api) getDescription(c *models.ReqContext) response.Response {
type resourcePermissionDTO struct {
ID int64 `json:"id"`
ResourceID string `json:"resourceId"`
RoleName string `json:"roleName"`
IsManaged bool `json:"isManaged"`
UserID int64 `json:"userId,omitempty"`
@ -105,9 +104,7 @@ func (a *api) getPermissions(c *models.ReqContext) response.Response {
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),
@ -117,6 +114,7 @@ func (a *api) getPermissions(c *models.ReqContext) response.Response {
BuiltInRole: p.BuiltInRole,
Actions: p.Actions,
Permission: permission,
IsManaged: p.IsManaged,
})
}
}

View File

@ -482,7 +482,7 @@ func TestApi_UidSolver(t *testing.T) {
}
}
func withSolver(options Options, solver uidSolver) Options {
func withSolver(options Options, solver UidSolver) Options {
options.UidSolver = solver
return options
}

View File

@ -1,7 +1,6 @@
package resourcepermissions
import (
"context"
"net/http"
"strconv"
@ -10,9 +9,7 @@ import (
"github.com/grafana/grafana/pkg/web"
)
type uidSolver func(ctx context.Context, orgID int64, uid string) (int64, error)
func solveUID(solve uidSolver) web.Handler {
func solveUID(solve UidSolver) web.Handler {
return func(c *models.ReqContext) {
if solve != nil && util.IsValidShortUID(web.Params(c.Req)[":resourceID"]) {
params := web.Params(c.Req)

View File

@ -7,7 +7,9 @@ import (
"github.com/grafana/grafana/pkg/services/sqlstore"
)
type UidSolver func(ctx context.Context, orgID int64, uid string) (int64, error)
type ResourceValidator func(ctx context.Context, orgID int64, resourceID string) error
type InheritedScopesSolver func(ctx context.Context, orgID int64, resourceID string) ([]string, error)
type Options struct {
// Resource is the action and scope prefix that is generated
@ -35,5 +37,7 @@ type Options struct {
// OnSetBuiltInRole if configured will be called each time a permission is set for a built-in role
OnSetBuiltInRole func(session *sqlstore.DBSession, orgID int64, builtInRole, resourceID, permission string) error
// UidSolver if configured will be used in a middleware to translate an uid to id for each request
UidSolver uidSolver
UidSolver UidSolver
// InheritedScopesSolver if configured can generate additional scopes that will be used when fetching permissions for a resource
InheritedScopesSolver InheritedScopesSolver
}

View File

@ -42,8 +42,8 @@ type Store interface {
hooks types.ResourceHooks,
) ([]accesscontrol.ResourcePermission, error)
// GetResourcesPermissions will return all permission for all supplied resource ids
GetResourcesPermissions(ctx context.Context, orgID int64, query types.GetResourcesPermissionsQuery) ([]accesscontrol.ResourcePermission, error)
// GetResourcePermissions will return all permission for supplied resource id
GetResourcePermissions(ctx context.Context, orgID int64, query types.GetResourcePermissionsQuery) ([]accesscontrol.ResourcePermission, error)
}
func New(options Options, cfg *setting.Cfg, router routing.RouteRegister, ac accesscontrol.AccessControl, store Store, sqlStore *sqlstore.SQLStore) (*Service, error) {
@ -101,12 +101,22 @@ type Service struct {
}
func (s *Service) GetPermissions(ctx context.Context, user *models.SignedInUser, resourceID string) ([]accesscontrol.ResourcePermission, error) {
return s.store.GetResourcesPermissions(ctx, user.OrgId, types.GetResourcesPermissionsQuery{
User: user,
Actions: s.actions,
Resource: s.options.Resource,
ResourceIDs: []string{resourceID},
OnlyManaged: s.options.OnlyManaged,
var inheritedScopes []string
if s.options.InheritedScopesSolver != nil {
var err error
inheritedScopes, err = s.options.InheritedScopesSolver(ctx, user.OrgId, resourceID)
if err != nil {
return nil, err
}
}
return s.store.GetResourcePermissions(ctx, user.OrgId, types.GetResourcePermissionsQuery{
User: user,
Actions: s.actions,
Resource: s.options.Resource,
ResourceID: resourceID,
InheritedScopes: inheritedScopes,
OnlyManaged: s.options.OnlyManaged,
})
}

View File

@ -20,10 +20,11 @@ type SetResourcePermissionsCommand struct {
SetResourcePermissionCommand
}
type GetResourcesPermissionsQuery struct {
Actions []string
Resource string
ResourceIDs []string
OnlyManaged bool
User *models.SignedInUser
type GetResourcePermissionsQuery struct {
Actions []string
Resource string
ResourceID string
OnlyManaged bool
InheritedScopes []string
User *models.SignedInUser
}

View File

@ -178,7 +178,7 @@ func (a *AccessControlDashboardGuardian) GetAcl() ([]*models.DashboardAclInfoDTO
acl := make([]*models.DashboardAclInfoDTO, 0, len(permissions))
for _, p := range permissions {
if !p.IsManaged() {
if !p.IsManaged {
continue
}

View File

@ -551,11 +551,11 @@ func TestAccessControlDashboardGuardian_GetHiddenACL(t *testing.T) {
{
desc: "should only return permissions containing hidden users",
permissions: []accesscontrol.ResourcePermission{
{RoleName: "managed:users:1:permissions", UserId: 1, UserLogin: "user1"},
{RoleName: "managed:teams:1:permissions", TeamId: 1, Team: "team1"},
{RoleName: "managed:users:2:permissions", UserId: 2, UserLogin: "user2"},
{RoleName: "managed:users:3:permissions", UserId: 3, UserLogin: "user3"},
{RoleName: "managed:users:4:permissions", UserId: 4, UserLogin: "user4"},
{RoleName: "managed:users:1:permissions", UserId: 1, UserLogin: "user1", IsManaged: true},
{RoleName: "managed:teams:1:permissions", TeamId: 1, Team: "team1", IsManaged: true},
{RoleName: "managed:users:2:permissions", UserId: 2, UserLogin: "user2", IsManaged: true},
{RoleName: "managed:users:3:permissions", UserId: 3, UserLogin: "user3", IsManaged: true},
{RoleName: "managed:users:4:permissions", UserId: 4, UserLogin: "user4", IsManaged: true},
},
hiddenUsers: map[string]struct{}{"user2": {}, "user3": {}},
},
@ -564,7 +564,6 @@ func TestAccessControlDashboardGuardian_GetHiddenACL(t *testing.T) {
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
guardian := setupAccessControlGuardianTest(t, 1, nil)
guardian.permissionServices.GetDashboardService()
mocked := accesscontrolmock.NewPermissionsServicesMock()
guardian.permissionServices = mocked