mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Access Control: Move part of access control database (#40483)
* Add accesscontrol migrations * Add ResourceStore interface and related structs * Add team/user/builtin-role * Add accesscontrol database with functions to handle managed roles and permissions * Add ResourceManager * Add GetUserPermissions * Update pkg/services/accesscontrol/accesscontrol.go Co-authored-by: Emil Tullstedt <emil.tullstedt@grafana.com>
This commit is contained in:
parent
8c98f24777
commit
3c659f1ea0
@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/registry"
|
"github.com/grafana/grafana/pkg/registry"
|
||||||
"github.com/grafana/grafana/pkg/server/backgroundsvcs"
|
"github.com/grafana/grafana/pkg/server/backgroundsvcs"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
acdb "github.com/grafana/grafana/pkg/services/accesscontrol/database"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/auth"
|
"github.com/grafana/grafana/pkg/services/auth"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
@ -58,6 +59,9 @@ var wireExtsBasicSet = wire.NewSet(
|
|||||||
wire.Bind(new(searchusers.Service), new(*searchusers.OSSService)),
|
wire.Bind(new(searchusers.Service), new(*searchusers.OSSService)),
|
||||||
signature.ProvideService,
|
signature.ProvideService,
|
||||||
wire.Bind(new(plugins.PluginLoaderAuthorizer), new(*signature.UnsignedPluginAuthorizer)),
|
wire.Bind(new(plugins.PluginLoaderAuthorizer), new(*signature.UnsignedPluginAuthorizer)),
|
||||||
|
acdb.ProvideService,
|
||||||
|
wire.Bind(new(accesscontrol.ResourceStore), new(*acdb.AccessControlStore)),
|
||||||
|
wire.Bind(new(accesscontrol.PermissionsProvider), new(*acdb.AccessControlStore)),
|
||||||
)
|
)
|
||||||
|
|
||||||
var wireExtsSet = wire.NewSet(
|
var wireExtsSet = wire.NewSet(
|
||||||
|
@ -25,6 +25,23 @@ type AccessControl interface {
|
|||||||
DeclareFixedRoles(...RoleRegistration) error
|
DeclareFixedRoles(...RoleRegistration) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PermissionsProvider interface {
|
||||||
|
GetUserPermissions(ctx context.Context, query GetUserPermissionsQuery) ([]*Permission, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResourceStore interface {
|
||||||
|
// SetUserResourcePermissions sets permissions for managed user role on a resource
|
||||||
|
SetUserResourcePermissions(ctx context.Context, orgID, userID int64, cmd SetResourcePermissionsCommand) ([]ResourcePermission, error)
|
||||||
|
// SetTeamResourcePermissions sets permissions for managed team role on a resource
|
||||||
|
SetTeamResourcePermissions(ctx context.Context, orgID, teamID int64, cmd SetResourcePermissionsCommand) ([]ResourcePermission, error)
|
||||||
|
// SetBuiltinResourcePermissions sets permissions for managed builtin role on a resource
|
||||||
|
SetBuiltinResourcePermissions(ctx context.Context, orgID int64, builtinRole string, cmd SetResourcePermissionsCommand) ([]ResourcePermission, error)
|
||||||
|
// RemoveResourcePermission remove permission for resource
|
||||||
|
RemoveResourcePermission(ctx context.Context, orgID int64, cmd RemoveResourcePermissionCommand) error
|
||||||
|
// GetResourcesPermissions will return all permission for all supplied resource ids
|
||||||
|
GetResourcesPermissions(ctx context.Context, orgID int64, query GetResourcesPermissionsQuery) ([]ResourcePermission, error)
|
||||||
|
}
|
||||||
|
|
||||||
func HasAccess(ac AccessControl, c *models.ReqContext) func(fallback func(*models.ReqContext) bool, evaluator Evaluator) bool {
|
func HasAccess(ac AccessControl, c *models.ReqContext) func(fallback func(*models.ReqContext) bool, evaluator Evaluator) bool {
|
||||||
return func(fallback func(*models.ReqContext) bool, evaluator Evaluator) bool {
|
return func(fallback func(*models.ReqContext) bool, evaluator Evaluator) bool {
|
||||||
if ac.IsDisabled() {
|
if ac.IsDisabled() {
|
||||||
|
101
pkg/services/accesscontrol/database/database.go
Normal file
101
pkg/services/accesscontrol/database/database.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
globalOrgID = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
func ProvideService(sqlStore *sqlstore.SQLStore) *AccessControlStore {
|
||||||
|
return &AccessControlStore{sqlStore}
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccessControlStore struct {
|
||||||
|
sql *sqlstore.SQLStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AccessControlStore) GetUserPermissions(ctx context.Context, query accesscontrol.GetUserPermissionsQuery) ([]*accesscontrol.Permission, error) {
|
||||||
|
result := make([]*accesscontrol.Permission, 0)
|
||||||
|
err := s.sql.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||||
|
filter, params := userRolesFilter(query.OrgID, query.UserID, query.Roles)
|
||||||
|
|
||||||
|
// TODO: optimize this
|
||||||
|
q := `SELECT
|
||||||
|
permission.id,
|
||||||
|
permission.role_id,
|
||||||
|
permission.action,
|
||||||
|
permission.scope,
|
||||||
|
permission.updated,
|
||||||
|
permission.created
|
||||||
|
FROM permission
|
||||||
|
INNER JOIN role ON role.id = permission.role_id
|
||||||
|
` + filter
|
||||||
|
|
||||||
|
if err := sess.SQL(q, params...).Find(&result); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func userRolesFilter(orgID, userID int64, roles []string) (string, []interface{}) {
|
||||||
|
q := `
|
||||||
|
WHERE role.id IN (
|
||||||
|
SELECT ur.role_id
|
||||||
|
FROM user_role AS ur
|
||||||
|
WHERE ur.user_id = ?
|
||||||
|
AND (ur.org_id = ? OR ur.org_id = ?)
|
||||||
|
UNION
|
||||||
|
SELECT tr.role_id FROM team_role as tr
|
||||||
|
INNER JOIN team_member as tm ON tm.team_id = tr.team_id
|
||||||
|
WHERE tm.user_id = ? AND tr.org_id = ?
|
||||||
|
`
|
||||||
|
params := []interface{}{userID, orgID, globalOrgID, userID, orgID}
|
||||||
|
|
||||||
|
if len(roles) != 0 {
|
||||||
|
q += `
|
||||||
|
UNION
|
||||||
|
SELECT br.role_id FROM builtin_role AS br
|
||||||
|
WHERE role IN (? ` + strings.Repeat(", ?", len(roles)-1) + `)
|
||||||
|
`
|
||||||
|
for _, role := range roles {
|
||||||
|
params = append(params, role)
|
||||||
|
}
|
||||||
|
|
||||||
|
q += `AND (br.org_id = ? OR br.org_id = ?)`
|
||||||
|
params = append(params, orgID, globalOrgID)
|
||||||
|
}
|
||||||
|
|
||||||
|
q += `)`
|
||||||
|
|
||||||
|
return q, params
|
||||||
|
}
|
||||||
|
|
||||||
|
func deletePermissions(sess *sqlstore.DBSession, ids []int64) error {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rawSQL := "DELETE FROM permission WHERE id IN(?" + strings.Repeat(",?", len(ids)-1) + ")"
|
||||||
|
args := make([]interface{}, 0, len(ids)+1)
|
||||||
|
args = append(args, rawSQL)
|
||||||
|
for _, id := range ids {
|
||||||
|
args = append(args, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := sess.Exec(args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
132
pkg/services/accesscontrol/database/database_test.go
Normal file
132
pkg/services/accesscontrol/database/database_test.go
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
|
)
|
||||||
|
|
||||||
|
type getUserPermissionsTestCase struct {
|
||||||
|
desc string
|
||||||
|
orgID int64
|
||||||
|
role string
|
||||||
|
userPermissions []string
|
||||||
|
teamPermissions []string
|
||||||
|
builtinPermissions []string
|
||||||
|
expected int
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccessControlStore_GetUserPermissions(t *testing.T) {
|
||||||
|
tests := []getUserPermissionsTestCase{
|
||||||
|
{
|
||||||
|
desc: "should successfully get user, team and builtin permissions",
|
||||||
|
orgID: 1,
|
||||||
|
role: "Admin",
|
||||||
|
userPermissions: []string{"1", "2", "10"},
|
||||||
|
teamPermissions: []string{"100", "2"},
|
||||||
|
builtinPermissions: []string{"5", "6"},
|
||||||
|
expected: 7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Should not get admin roles",
|
||||||
|
orgID: 1,
|
||||||
|
role: "Viewer",
|
||||||
|
userPermissions: []string{"1", "2", "10"},
|
||||||
|
teamPermissions: []string{"100", "2"},
|
||||||
|
builtinPermissions: []string{"5", "6"},
|
||||||
|
expected: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Should work without org role",
|
||||||
|
orgID: 1,
|
||||||
|
role: "",
|
||||||
|
userPermissions: []string{"1", "2", "10"},
|
||||||
|
teamPermissions: []string{"100", "2"},
|
||||||
|
builtinPermissions: []string{"5", "6"},
|
||||||
|
expected: 5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
|
store, sql := setupTestEnv(t)
|
||||||
|
|
||||||
|
user, team := createUserAndTeam(t, sql, tt.orgID)
|
||||||
|
|
||||||
|
for _, id := range tt.userPermissions {
|
||||||
|
_, err := store.SetUserResourcePermissions(context.Background(), tt.orgID, user.Id, accesscontrol.SetResourcePermissionsCommand{
|
||||||
|
Actions: []string{"dashboards:read"},
|
||||||
|
Resource: "dashboards",
|
||||||
|
ResourceID: id,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, id := range tt.teamPermissions {
|
||||||
|
_, err := store.SetTeamResourcePermissions(context.Background(), tt.orgID, team.Id, accesscontrol.SetResourcePermissionsCommand{
|
||||||
|
Actions: []string{"dashboards:read"},
|
||||||
|
Resource: "dashboards",
|
||||||
|
ResourceID: id,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, id := range tt.builtinPermissions {
|
||||||
|
_, err := store.SetBuiltinResourcePermissions(context.Background(), tt.orgID, "Admin", accesscontrol.SetResourcePermissionsCommand{
|
||||||
|
Actions: []string{"dashboards:read"},
|
||||||
|
Resource: "dashboards",
|
||||||
|
ResourceID: id,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var roles []string
|
||||||
|
role := models.RoleType(tt.role)
|
||||||
|
|
||||||
|
if role.IsValid() {
|
||||||
|
roles = append(roles, string(role))
|
||||||
|
for _, c := range role.Children() {
|
||||||
|
roles = append(roles, string(c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
permissions, err := store.GetUserPermissions(context.Background(), accesscontrol.GetUserPermissionsQuery{
|
||||||
|
OrgID: tt.orgID,
|
||||||
|
UserID: user.Id,
|
||||||
|
Roles: roles,
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, permissions, tt.expected)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createUserAndTeam(t *testing.T, sql *sqlstore.SQLStore, orgID int64) (*models.User, models.Team) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
user, err := sql.CreateUser(context.Background(), models.CreateUserCommand{
|
||||||
|
Login: "user",
|
||||||
|
OrgId: orgID,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
team, err := sql.CreateTeam("team", "", orgID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = sql.AddTeamMember(user.Id, orgID, team.Id, false, models.PERMISSION_VIEW)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return user, team
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupTestEnv(t testing.TB) (*AccessControlStore, *sqlstore.SQLStore) {
|
||||||
|
store := sqlstore.InitTestDB(t)
|
||||||
|
return ProvideService(store), store
|
||||||
|
}
|
519
pkg/services/accesscontrol/database/resource.go
Normal file
519
pkg/services/accesscontrol/database/resource.go
Normal file
@ -0,0 +1,519 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *AccessControlStore) SetUserResourcePermissions(ctx context.Context, orgID, userID int64, cmd accesscontrol.SetResourcePermissionsCommand) ([]accesscontrol.ResourcePermission, error) {
|
||||||
|
if userID == 0 {
|
||||||
|
return nil, models.ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var permissions []accesscontrol.ResourcePermission
|
||||||
|
err = s.sql.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||||
|
permissions, err = s.setResourcePermissions(sess, orgID, managedUserRoleName(userID), s.userAdder(sess, orgID, userID), cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AccessControlStore) SetTeamResourcePermissions(ctx context.Context, orgID, teamID int64, cmd accesscontrol.SetResourcePermissionsCommand) ([]accesscontrol.ResourcePermission, error) {
|
||||||
|
if teamID == 0 {
|
||||||
|
return nil, models.ErrTeamNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var permissions []accesscontrol.ResourcePermission
|
||||||
|
err = s.sql.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||||
|
permissions, err = s.setResourcePermissions(sess, orgID, managedTeamRoleName(teamID), s.teamAdder(sess, orgID, teamID), cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AccessControlStore) SetBuiltinResourcePermissions(ctx context.Context, orgID int64, builtinRole string, cmd accesscontrol.SetResourcePermissionsCommand) ([]accesscontrol.ResourcePermission, error) {
|
||||||
|
if !models.RoleType(builtinRole).IsValid() || builtinRole == accesscontrol.RoleGrafanaAdmin {
|
||||||
|
return nil, fmt.Errorf("invalid role: %s", builtinRole)
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var permissions []accesscontrol.ResourcePermission
|
||||||
|
|
||||||
|
err = s.sql.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||||
|
permissions, err = s.setResourcePermissions(sess, orgID, managedBuiltInRoleName(builtinRole), s.builtinRoleAdder(sess, orgID, builtinRole), cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type roleAdder func(roleID int64) error
|
||||||
|
|
||||||
|
func (s *AccessControlStore) setResourcePermissions(
|
||||||
|
sess *sqlstore.DBSession, orgID int64, roleName string, adder roleAdder, cmd accesscontrol.SetResourcePermissionsCommand,
|
||||||
|
) ([]accesscontrol.ResourcePermission, error) {
|
||||||
|
role, err := s.getOrCreateManagedRole(sess, orgID, roleName, adder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rawSQL := `
|
||||||
|
SELECT
|
||||||
|
p.*
|
||||||
|
FROM permission as p
|
||||||
|
INNER JOIN role r on r.id = p.role_id
|
||||||
|
WHERE r.id = ?
|
||||||
|
AND p.scope = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
var current []accesscontrol.Permission
|
||||||
|
if err := sess.SQL(rawSQL, role.ID, getResourceScope(cmd.Resource, cmd.ResourceID)).Find(¤t); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
missing := make(map[string]struct{}, len(cmd.Actions))
|
||||||
|
for _, a := range cmd.Actions {
|
||||||
|
missing[a] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var keep []int64
|
||||||
|
var remove []int64
|
||||||
|
for _, p := range current {
|
||||||
|
if _, ok := missing[p.Action]; ok {
|
||||||
|
keep = append(keep, p.ID)
|
||||||
|
delete(missing, p.Action)
|
||||||
|
} else if !ok {
|
||||||
|
remove = append(remove, p.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := deletePermissions(sess, remove); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var permissions []accesscontrol.ResourcePermission
|
||||||
|
|
||||||
|
for action := range missing {
|
||||||
|
p, err := createResourcePermission(sess, role.ID, action, cmd.Resource, cmd.ResourceID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
permissions = append(permissions, *p)
|
||||||
|
}
|
||||||
|
|
||||||
|
keptPermissions, err := getManagedPermissions(sess, cmd.ResourceID, keep)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
permissions = append(permissions, keptPermissions...)
|
||||||
|
return permissions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AccessControlStore) RemoveResourcePermission(ctx context.Context, orgID int64, cmd accesscontrol.RemoveResourcePermissionCommand) error {
|
||||||
|
return s.sql.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||||
|
var permission accesscontrol.Permission
|
||||||
|
rawSql := `
|
||||||
|
SELECT
|
||||||
|
p.*
|
||||||
|
FROM permission p
|
||||||
|
LEFT JOIN role r ON p.role_id = r.id
|
||||||
|
WHERE r.name LIKE 'managed:%'
|
||||||
|
AND r.org_id = ?
|
||||||
|
AND p.id = ?
|
||||||
|
AND p.scope = ?
|
||||||
|
AND p.action IN(?` + strings.Repeat(",?", len(cmd.Actions)-1) + `)
|
||||||
|
`
|
||||||
|
|
||||||
|
args := []interface{}{
|
||||||
|
orgID,
|
||||||
|
cmd.PermissionID,
|
||||||
|
getResourceScope(cmd.Resource, cmd.ResourceID),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, a := range cmd.Actions {
|
||||||
|
args = append(args, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, err := sess.SQL(rawSql, args...).Get(&permission)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return deletePermissions(sess, []int64{permission.ID})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AccessControlStore) GetResourcesPermissions(ctx context.Context, orgID int64, query accesscontrol.GetResourcesPermissionsQuery) ([]accesscontrol.ResourcePermission, error) {
|
||||||
|
var result []accesscontrol.ResourcePermission
|
||||||
|
|
||||||
|
err := s.sql.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||||
|
var err error
|
||||||
|
result, err = getResourcesPermissions(sess, orgID, query, false)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func createResourcePermission(sess *sqlstore.DBSession, roleID int64, action, resource string, resourceID string) (*accesscontrol.ResourcePermission, error) {
|
||||||
|
permission := managedPermission(action, resource, resourceID)
|
||||||
|
permission.RoleID = roleID
|
||||||
|
permission.Created = time.Now()
|
||||||
|
permission.Updated = time.Now()
|
||||||
|
|
||||||
|
if _, err := sess.Insert(&permission); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rawSql := `
|
||||||
|
SELECT
|
||||||
|
p.*,
|
||||||
|
? AS resource_id,
|
||||||
|
ur.user_id AS user_id,
|
||||||
|
u.login AS user_login,
|
||||||
|
u.email AS user_email,
|
||||||
|
tr.team_id AS team_id,
|
||||||
|
t.name AS team,
|
||||||
|
t.email AS team_email,
|
||||||
|
r.name as role_name
|
||||||
|
FROM permission p
|
||||||
|
LEFT JOIN role r ON p.role_id = r.id
|
||||||
|
LEFT JOIN team_role tr ON r.id = tr.role_id
|
||||||
|
LEFT JOIN team t ON tr.team_id = t.id
|
||||||
|
LEFT JOIN user_role ur ON r.id = ur.role_id
|
||||||
|
LEFT JOIN user u ON ur.user_id = u.id
|
||||||
|
WHERE p.id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
p := &accesscontrol.ResourcePermission{}
|
||||||
|
if _, err := sess.SQL(rawSql, resourceID, permission.ID).Get(p); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getResourcesPermissions(sess *sqlstore.DBSession, orgID int64, query accesscontrol.GetResourcesPermissionsQuery, managed bool) ([]accesscontrol.ResourcePermission, error) {
|
||||||
|
result := make([]accesscontrol.ResourcePermission, 0)
|
||||||
|
|
||||||
|
if len(query.Actions) == 0 {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(query.ResourceIDs) == 0 {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rawSelect := `
|
||||||
|
SELECT
|
||||||
|
p.*,
|
||||||
|
r.name as role_name,
|
||||||
|
`
|
||||||
|
|
||||||
|
userSelect := rawSelect + `
|
||||||
|
ur.user_id AS user_id,
|
||||||
|
u.login AS user_login,
|
||||||
|
u.email AS user_email,
|
||||||
|
0 AS team_id,
|
||||||
|
'' AS team,
|
||||||
|
'' AS team_email,
|
||||||
|
'' AS built_in_role
|
||||||
|
`
|
||||||
|
|
||||||
|
teamSelect := rawSelect + `
|
||||||
|
0 AS user_id,
|
||||||
|
'' AS user_login,
|
||||||
|
'' AS user_email,
|
||||||
|
tr.team_id AS team_id,
|
||||||
|
t.name AS team,
|
||||||
|
t.email AS team_email,
|
||||||
|
'' AS built_in_role
|
||||||
|
`
|
||||||
|
|
||||||
|
builtinSelect := rawSelect + `
|
||||||
|
0 AS user_id,
|
||||||
|
'' AS user_login,
|
||||||
|
'' AS user_email,
|
||||||
|
0 as team_id,
|
||||||
|
'' AS team,
|
||||||
|
'' AS team_email,
|
||||||
|
br.role AS built_in_role
|
||||||
|
`
|
||||||
|
|
||||||
|
rawFrom := `
|
||||||
|
FROM permission p
|
||||||
|
INNER JOIN role r ON p.role_id = r.id
|
||||||
|
`
|
||||||
|
userFrom := rawFrom + `
|
||||||
|
INNER JOIN user_role ur ON r.id = ur.role_id
|
||||||
|
INNER JOIN user u ON ur.user_id = u.id
|
||||||
|
`
|
||||||
|
teamFrom := rawFrom + `
|
||||||
|
INNER JOIN team_role tr ON r.id = tr.role_id
|
||||||
|
INNER JOIN team t ON tr.team_id = t.id
|
||||||
|
`
|
||||||
|
|
||||||
|
builtinFrom := rawFrom + `
|
||||||
|
INNER JOIN builtin_role br ON r.id = br.role_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 managed {
|
||||||
|
where += `AND r.name LIKE 'managed:%'`
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []interface{}{
|
||||||
|
orgID,
|
||||||
|
getResourceAllScope(query.Resource),
|
||||||
|
getResourceAllIDScope(query.Resource),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, id := range query.ResourceIDs {
|
||||||
|
args = append(args, getResourceScope(query.Resource, id))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, a := range query.Actions {
|
||||||
|
args = append(args, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need args x3 due to union
|
||||||
|
initialLength := len(args)
|
||||||
|
args = append(args, args[:initialLength]...)
|
||||||
|
args = append(args, args[:initialLength]...)
|
||||||
|
|
||||||
|
user := userSelect + userFrom + where
|
||||||
|
team := teamSelect + teamFrom + where
|
||||||
|
builtin := builtinSelect + builtinFrom + where
|
||||||
|
sql := user + "UNION" + team + "UNION" + builtin
|
||||||
|
|
||||||
|
if err := sess.SQL(sql, args...).Find(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
scopeAll := getResourceAllScope(query.Resource)
|
||||||
|
scopeAllIDs := getResourceAllIDScope(query.Resource)
|
||||||
|
out := make([]accesscontrol.ResourcePermission, 0, len(result))
|
||||||
|
|
||||||
|
// Add resourceIds and generate permissions for `*`, `resource:*` and `resource:id:*`
|
||||||
|
// TODO: handle scope with other key prefixes e.g. `resource:name:*` and `resource:name:name`
|
||||||
|
for _, id := range query.ResourceIDs {
|
||||||
|
scope := getResourceScope(query.Resource, id)
|
||||||
|
for _, p := range result {
|
||||||
|
if p.Scope == scope || p.Scope == scopeAll || p.Scope == scopeAllIDs || p.Scope == "*" {
|
||||||
|
p.ResourceID = id
|
||||||
|
out = append(out, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AccessControlStore) userAdder(sess *sqlstore.DBSession, orgID, userID int64) roleAdder {
|
||||||
|
return func(roleID int64) error {
|
||||||
|
if res, err := sess.Query("SELECT 1 FROM user_role WHERE org_id=? AND user_id=? AND role_id=?", orgID, userID, roleID); err != nil {
|
||||||
|
return err
|
||||||
|
} else if len(res) == 1 {
|
||||||
|
return fmt.Errorf("role is already added to this user")
|
||||||
|
}
|
||||||
|
|
||||||
|
userRole := &accesscontrol.UserRole{
|
||||||
|
OrgID: orgID,
|
||||||
|
UserID: userID,
|
||||||
|
RoleID: roleID,
|
||||||
|
Created: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := sess.Insert(userRole)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AccessControlStore) teamAdder(sess *sqlstore.DBSession, orgID, teamID int64) roleAdder {
|
||||||
|
return func(roleID int64) error {
|
||||||
|
if res, err := sess.Query("SELECT 1 FROM team_role WHERE org_id=? AND team_id=? AND role_id=?", orgID, teamID, roleID); err != nil {
|
||||||
|
return err
|
||||||
|
} else if len(res) == 1 {
|
||||||
|
return fmt.Errorf("role is already added to this team")
|
||||||
|
}
|
||||||
|
|
||||||
|
teamRole := &accesscontrol.TeamRole{
|
||||||
|
OrgID: orgID,
|
||||||
|
TeamID: teamID,
|
||||||
|
RoleID: roleID,
|
||||||
|
Created: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := sess.Insert(teamRole)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AccessControlStore) builtinRoleAdder(sess *sqlstore.DBSession, orgID int64, builtinRole string) roleAdder {
|
||||||
|
return func(roleID int64) error {
|
||||||
|
if res, err := sess.Query("SELECT 1 FROM builtin_role WHERE role_id=? AND role=? AND org_id=?", roleID, builtinRole, orgID); err != nil {
|
||||||
|
return err
|
||||||
|
} else if len(res) == 1 {
|
||||||
|
return fmt.Errorf("built-in role already has the role granted")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := sess.Table("builtin_role").Insert(accesscontrol.BuiltinRole{
|
||||||
|
RoleID: roleID,
|
||||||
|
OrgID: orgID,
|
||||||
|
Role: builtinRole,
|
||||||
|
Updated: time.Now(),
|
||||||
|
Created: time.Now(),
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AccessControlStore) getOrCreateManagedRole(sess *sqlstore.DBSession, orgID int64, name string, add roleAdder) (*accesscontrol.Role, error) {
|
||||||
|
role := accesscontrol.Role{OrgID: orgID, Name: name}
|
||||||
|
has, err := sess.Where("org_id = ? AND name = ?", orgID, name).Get(&role)
|
||||||
|
|
||||||
|
// If managed role does not exist, create it and add it to user/team/builtin
|
||||||
|
if !has {
|
||||||
|
uid, err := generateNewRoleUID(sess, orgID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
role = accesscontrol.Role{
|
||||||
|
OrgID: orgID,
|
||||||
|
Name: name,
|
||||||
|
UID: uid,
|
||||||
|
Created: time.Now(),
|
||||||
|
Updated: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := sess.Insert(&role); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := add(role.ID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &role, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getManagedPermissions(sess *sqlstore.DBSession, resourceID string, ids []int64) ([]accesscontrol.ResourcePermission, error) {
|
||||||
|
var result []accesscontrol.ResourcePermission
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rawSql := `
|
||||||
|
SELECT
|
||||||
|
p.*,
|
||||||
|
? AS resource_id,
|
||||||
|
ur.user_id AS user_id,
|
||||||
|
u.login AS user_login,
|
||||||
|
u.email AS user_email,
|
||||||
|
tr.team_id AS team_id,
|
||||||
|
t.name AS team,
|
||||||
|
t.email AS team_email,
|
||||||
|
r.name as role_name
|
||||||
|
FROM permission p
|
||||||
|
INNER JOIN role r ON p.role_id = r.id
|
||||||
|
LEFT JOIN team_role tr ON r.id = tr.role_id
|
||||||
|
LEFT JOIN team t ON tr.team_id = t.id
|
||||||
|
LEFT JOIN user_role ur ON r.id = ur.role_id
|
||||||
|
LEFT JOIN user u ON ur.user_id = u.id
|
||||||
|
WHERE p.id IN (?` + strings.Repeat(",?", len(ids)-1) + `)
|
||||||
|
`
|
||||||
|
|
||||||
|
args := make([]interface{}, 0, len(ids)+1)
|
||||||
|
args = append(args, resourceID)
|
||||||
|
for _, id := range ids {
|
||||||
|
args = append(args, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sess.SQL(rawSql, args...).Find(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func managedPermission(action, resource string, resourceID string) accesscontrol.Permission {
|
||||||
|
return accesscontrol.Permission{
|
||||||
|
Action: action,
|
||||||
|
Scope: getResourceScope(resource, resourceID),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func managedUserRoleName(userID int64) string {
|
||||||
|
return fmt.Sprintf("managed:users:%d:permissions", userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func managedTeamRoleName(teamID int64) string {
|
||||||
|
return fmt.Sprintf("managed:teams:%d:permissions", teamID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func managedBuiltInRoleName(builtinRole string) string {
|
||||||
|
return fmt.Sprintf("managed:builtins:%s:permissions", strings.ToLower(builtinRole))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getResourceScope(resource string, resourceID string) string {
|
||||||
|
return fmt.Sprintf("%s:id:%s", resource, resourceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getResourceAllScope(resource string) string {
|
||||||
|
return fmt.Sprintf("%s:*", resource)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getResourceAllIDScope(resource string) string {
|
||||||
|
return fmt.Sprintf("%s:id:*", resource)
|
||||||
|
}
|
159
pkg/services/accesscontrol/database/resource_bench_test.go
Normal file
159
pkg/services/accesscontrol/database/resource_bench_test.go
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dsAction = "datasources:query"
|
||||||
|
dsResource = "datasources"
|
||||||
|
PermissionsPerRole = 10
|
||||||
|
UsersPerTeam = 10
|
||||||
|
permissionsPerDs = 100
|
||||||
|
)
|
||||||
|
|
||||||
|
func BenchmarkDSPermissions10_10(b *testing.B) { benchmarkDSPermissions(b, 10, 10) }
|
||||||
|
|
||||||
|
func BenchmarkDSPermissions10_100(b *testing.B) { benchmarkDSPermissions(b, 10, 100) }
|
||||||
|
|
||||||
|
func BenchmarkDSPermissions10_1000(b *testing.B) { benchmarkDSPermissions(b, 10, 1000) }
|
||||||
|
|
||||||
|
func BenchmarkDSPermissions100_10(b *testing.B) { benchmarkDSPermissions(b, 100, 10) }
|
||||||
|
|
||||||
|
func BenchmarkDSPermissions100_100(b *testing.B) { benchmarkDSPermissions(b, 100, 100) }
|
||||||
|
|
||||||
|
func BenchmarkDSPermissions100_1000(b *testing.B) { benchmarkDSPermissions(b, 100, 1000) }
|
||||||
|
|
||||||
|
func BenchmarkDSPermissions1000_10(b *testing.B) { benchmarkDSPermissions(b, 1000, 10) }
|
||||||
|
|
||||||
|
func BenchmarkDSPermissions1000_100(b *testing.B) { benchmarkDSPermissions(b, 1000, 100) }
|
||||||
|
|
||||||
|
func BenchmarkDSPermissions1000_1000(b *testing.B) { benchmarkDSPermissions(b, 1000, 1000) }
|
||||||
|
|
||||||
|
func benchmarkDSPermissions(b *testing.B, dsNum, usersNum int) {
|
||||||
|
ac, dataSources := setupResourceBenchmark(b, dsNum, usersNum)
|
||||||
|
// We don't want to measure DB initialization
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
getDSPermissions(b, ac, dataSources)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDSPermissions(b *testing.B, store accesscontrol.ResourceStore, dataSources []int64) {
|
||||||
|
dsId := dataSources[0]
|
||||||
|
|
||||||
|
permissions, err := store.GetResourcesPermissions(context.Background(), accesscontrol.GlobalOrgID, accesscontrol.GetResourcesPermissionsQuery{
|
||||||
|
Actions: []string{dsAction},
|
||||||
|
Resource: dsResource,
|
||||||
|
ResourceIDs: []string{strconv.Itoa(int(dsId))},
|
||||||
|
})
|
||||||
|
require.NoError(b, err)
|
||||||
|
assert.GreaterOrEqual(b, len(permissions), 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupResourceBenchmark(b *testing.B, dsNum, usersNum int) (*AccessControlStore, []int64) {
|
||||||
|
ac, sql := setupTestEnv(b)
|
||||||
|
dataSources := GenerateDatasourcePermissions(b, sql, ac, dsNum, usersNum, permissionsPerDs)
|
||||||
|
return ac, dataSources
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateDatasourcePermissions(b *testing.B, db *sqlstore.SQLStore, ac *AccessControlStore, dsNum, usersNum, permissionsPerDs int) []int64 {
|
||||||
|
dataSources := make([]int64, 0)
|
||||||
|
for i := 0; i < dsNum; i++ {
|
||||||
|
addDSCommand := &models.AddDataSourceCommand{
|
||||||
|
OrgId: 0,
|
||||||
|
Name: fmt.Sprintf("ds_%d", i),
|
||||||
|
Type: models.DS_GRAPHITE,
|
||||||
|
Access: models.DS_ACCESS_DIRECT,
|
||||||
|
Url: "http://test",
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = db.AddDataSource(context.Background(), addDSCommand)
|
||||||
|
dataSources = append(dataSources, addDSCommand.Result.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
userIds, teamIds := generateTeamsAndUsers(b, db, usersNum)
|
||||||
|
|
||||||
|
for _, dsID := range dataSources {
|
||||||
|
// Add DS permissions for the users
|
||||||
|
maxPermissions := int(math.Min(float64(permissionsPerDs), float64(len(userIds))))
|
||||||
|
for i := 0; i < maxPermissions; i++ {
|
||||||
|
_, err := ac.SetUserResourcePermissions(
|
||||||
|
context.Background(),
|
||||||
|
accesscontrol.GlobalOrgID,
|
||||||
|
userIds[i],
|
||||||
|
accesscontrol.SetResourcePermissionsCommand{
|
||||||
|
Actions: []string{dsAction},
|
||||||
|
Resource: dsResource,
|
||||||
|
ResourceID: strconv.Itoa(int(dsID)),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
require.NoError(b, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add DS permissions for the teams
|
||||||
|
maxPermissions = int(math.Min(float64(permissionsPerDs), float64(len(teamIds))))
|
||||||
|
for i := 0; i < maxPermissions; i++ {
|
||||||
|
_, err := ac.SetTeamResourcePermissions(
|
||||||
|
context.Background(),
|
||||||
|
accesscontrol.GlobalOrgID,
|
||||||
|
teamIds[i],
|
||||||
|
accesscontrol.SetResourcePermissionsCommand{
|
||||||
|
Actions: []string{"datasources:query"},
|
||||||
|
Resource: "datasources",
|
||||||
|
ResourceID: strconv.Itoa(int(dsID)),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
require.NoError(b, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataSources
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateTeamsAndUsers(b *testing.B, db *sqlstore.SQLStore, users int) ([]int64, []int64) {
|
||||||
|
numberOfTeams := int(math.Ceil(float64(users) / UsersPerTeam))
|
||||||
|
globalUserId := 0
|
||||||
|
|
||||||
|
userIds := make([]int64, 0)
|
||||||
|
teamIds := make([]int64, 0)
|
||||||
|
for i := 0; i < numberOfTeams; i++ {
|
||||||
|
// Create team
|
||||||
|
teamName := fmt.Sprintf("%s%v", "team", i)
|
||||||
|
teamEmail := fmt.Sprintf("%s@example.org", teamName)
|
||||||
|
team, err := db.CreateTeam(teamName, teamEmail, 1)
|
||||||
|
require.NoError(b, err)
|
||||||
|
teamId := team.Id
|
||||||
|
teamIds = append(teamIds, teamId)
|
||||||
|
|
||||||
|
// Create team users
|
||||||
|
for u := 0; u < UsersPerTeam; u++ {
|
||||||
|
userName := fmt.Sprintf("%s%v", "user", globalUserId)
|
||||||
|
userEmail := fmt.Sprintf("%s@example.org", userName)
|
||||||
|
createUserCmd := models.CreateUserCommand{Email: userEmail, Name: userName, Login: userName, OrgId: 1}
|
||||||
|
|
||||||
|
user, err := db.CreateUser(context.Background(), createUserCmd)
|
||||||
|
require.NoError(b, err)
|
||||||
|
userId := user.Id
|
||||||
|
globalUserId++
|
||||||
|
userIds = append(userIds, userId)
|
||||||
|
|
||||||
|
err = db.AddTeamMember(userId, 1, teamId, false, 1)
|
||||||
|
require.NoError(b, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return userIds, teamIds
|
||||||
|
}
|
391
pkg/services/accesscontrol/database/resource_test.go
Normal file
391
pkg/services/accesscontrol/database/resource_test.go
Normal file
@ -0,0 +1,391 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
|
)
|
||||||
|
|
||||||
|
type setUserResourcePermissionsTest struct {
|
||||||
|
desc string
|
||||||
|
orgID int64
|
||||||
|
userID int64
|
||||||
|
actions []string
|
||||||
|
resource string
|
||||||
|
resourceID string
|
||||||
|
seeds []accesscontrol.SetResourcePermissionsCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccessControlStore_SetUserResourcePermissions(t *testing.T) {
|
||||||
|
tests := []setUserResourcePermissionsTest{
|
||||||
|
{
|
||||||
|
desc: "should set resource permission for user",
|
||||||
|
userID: 1,
|
||||||
|
actions: []string{"datasources:query"},
|
||||||
|
resource: "datasources",
|
||||||
|
resourceID: "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should remove resource permission for user",
|
||||||
|
orgID: 1,
|
||||||
|
userID: 1,
|
||||||
|
actions: []string{},
|
||||||
|
resource: "datasources",
|
||||||
|
resourceID: "1",
|
||||||
|
seeds: []accesscontrol.SetResourcePermissionsCommand{
|
||||||
|
{
|
||||||
|
Actions: []string{"datasources:query"},
|
||||||
|
Resource: "datasources",
|
||||||
|
ResourceID: "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should add new resource permission for user",
|
||||||
|
orgID: 1,
|
||||||
|
userID: 1,
|
||||||
|
actions: []string{"datasources:query", "datasources:write"},
|
||||||
|
resource: "datasources",
|
||||||
|
resourceID: "1",
|
||||||
|
seeds: []accesscontrol.SetResourcePermissionsCommand{
|
||||||
|
{
|
||||||
|
Actions: []string{"datasources:write"},
|
||||||
|
Resource: "datasources",
|
||||||
|
ResourceID: "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
store, _ := setupTestEnv(t)
|
||||||
|
|
||||||
|
for _, s := range test.seeds {
|
||||||
|
_, err := store.SetUserResourcePermissions(context.Background(), test.orgID, test.userID, s)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
added, err := store.SetUserResourcePermissions(context.Background(), test.userID, test.userID, accesscontrol.SetResourcePermissionsCommand{
|
||||||
|
Actions: test.actions,
|
||||||
|
Resource: test.resource,
|
||||||
|
ResourceID: test.resourceID,
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, added, len(test.actions))
|
||||||
|
for _, p := range added {
|
||||||
|
assert.Equal(t, getResourceScope(test.resource, test.resourceID), p.Scope)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type setTeamResourcePermissionsTest struct {
|
||||||
|
desc string
|
||||||
|
orgID int64
|
||||||
|
teamID int64
|
||||||
|
actions []string
|
||||||
|
resource string
|
||||||
|
resourceID string
|
||||||
|
seeds []accesscontrol.SetResourcePermissionsCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccessControlStore_SetTeamResourcePermissions(t *testing.T) {
|
||||||
|
tests := []setTeamResourcePermissionsTest{
|
||||||
|
{
|
||||||
|
desc: "should add new resource permission for team",
|
||||||
|
orgID: 1,
|
||||||
|
teamID: 1,
|
||||||
|
actions: []string{"datasources:query"},
|
||||||
|
resource: "datasources",
|
||||||
|
resourceID: "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should add new resource permission when others exist",
|
||||||
|
orgID: 1,
|
||||||
|
teamID: 1,
|
||||||
|
actions: []string{"datasources:query", "datasources:write"},
|
||||||
|
resource: "datasources",
|
||||||
|
resourceID: "1",
|
||||||
|
seeds: []accesscontrol.SetResourcePermissionsCommand{
|
||||||
|
{
|
||||||
|
Actions: []string{"datasources:query"},
|
||||||
|
Resource: "datasources",
|
||||||
|
ResourceID: "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should remove permissions for team",
|
||||||
|
orgID: 1,
|
||||||
|
teamID: 1,
|
||||||
|
actions: []string{},
|
||||||
|
resource: "datasources",
|
||||||
|
resourceID: "1",
|
||||||
|
seeds: []accesscontrol.SetResourcePermissionsCommand{
|
||||||
|
{
|
||||||
|
Actions: []string{"datasources:query"},
|
||||||
|
Resource: "datasources",
|
||||||
|
ResourceID: "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
store, _ := setupTestEnv(t)
|
||||||
|
|
||||||
|
for _, s := range test.seeds {
|
||||||
|
_, err := store.SetTeamResourcePermissions(context.Background(), test.orgID, test.teamID, s)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
added, err := store.SetTeamResourcePermissions(context.Background(), test.orgID, test.teamID, accesscontrol.SetResourcePermissionsCommand{
|
||||||
|
Actions: test.actions,
|
||||||
|
Resource: test.resource,
|
||||||
|
ResourceID: test.resourceID,
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, added, len(test.actions))
|
||||||
|
for _, p := range added {
|
||||||
|
assert.Equal(t, getResourceScope(test.resource, test.resourceID), p.Scope)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type setBuiltinResourcePermissionsTest struct {
|
||||||
|
desc string
|
||||||
|
orgID int64
|
||||||
|
builtinRole string
|
||||||
|
actions []string
|
||||||
|
resource string
|
||||||
|
resourceID string
|
||||||
|
seeds []accesscontrol.SetResourcePermissionsCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccessControlStore_SetBuiltinResourcePermissions(t *testing.T) {
|
||||||
|
tests := []setBuiltinResourcePermissionsTest{
|
||||||
|
{
|
||||||
|
desc: "should add new resource permission for builtin role",
|
||||||
|
orgID: 1,
|
||||||
|
builtinRole: "Viewer",
|
||||||
|
actions: []string{"datasources:query"},
|
||||||
|
resource: "datasources",
|
||||||
|
resourceID: "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should add new resource permission when others exist",
|
||||||
|
orgID: 1,
|
||||||
|
builtinRole: "Viewer",
|
||||||
|
actions: []string{"datasources:query", "datasources:write"},
|
||||||
|
resource: "datasources",
|
||||||
|
resourceID: "1",
|
||||||
|
seeds: []accesscontrol.SetResourcePermissionsCommand{
|
||||||
|
{
|
||||||
|
Actions: []string{"datasources:query"},
|
||||||
|
Resource: "datasources",
|
||||||
|
ResourceID: "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should remove permissions for builtin role",
|
||||||
|
orgID: 1,
|
||||||
|
builtinRole: "Viewer",
|
||||||
|
actions: []string{},
|
||||||
|
resource: "datasources",
|
||||||
|
resourceID: "1",
|
||||||
|
seeds: []accesscontrol.SetResourcePermissionsCommand{
|
||||||
|
{
|
||||||
|
Actions: []string{"datasources:query"},
|
||||||
|
Resource: "datasources",
|
||||||
|
ResourceID: "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
store, _ := setupTestEnv(t)
|
||||||
|
|
||||||
|
for _, s := range test.seeds {
|
||||||
|
_, err := store.SetBuiltinResourcePermissions(context.Background(), test.orgID, test.builtinRole, s)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
added, err := store.SetBuiltinResourcePermissions(context.Background(), test.orgID, test.builtinRole, accesscontrol.SetResourcePermissionsCommand{
|
||||||
|
Actions: test.actions,
|
||||||
|
Resource: test.resource,
|
||||||
|
ResourceID: test.resourceID,
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, added, len(test.actions))
|
||||||
|
for _, p := range added {
|
||||||
|
assert.Equal(t, getResourceScope(test.resource, test.resourceID), p.Scope)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type resourcePermission struct {
|
||||||
|
resource string
|
||||||
|
resourceID string
|
||||||
|
}
|
||||||
|
|
||||||
|
type removeResourcePermissionTest struct {
|
||||||
|
desc string
|
||||||
|
add resourcePermission
|
||||||
|
remove resourcePermission
|
||||||
|
expectedErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccessControlStore_RemoveResourcePermission(t *testing.T) {
|
||||||
|
tests := []removeResourcePermissionTest{
|
||||||
|
{
|
||||||
|
desc: "should remove resource permission",
|
||||||
|
add: resourcePermission{
|
||||||
|
resource: "datasources",
|
||||||
|
resourceID: "1",
|
||||||
|
},
|
||||||
|
remove: resourcePermission{
|
||||||
|
resource: "datasources",
|
||||||
|
resourceID: "1",
|
||||||
|
},
|
||||||
|
expectedErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should return nil when permission does not exist",
|
||||||
|
add: resourcePermission{
|
||||||
|
resource: "datasources",
|
||||||
|
resourceID: "1",
|
||||||
|
},
|
||||||
|
remove: resourcePermission{
|
||||||
|
resource: "datasources",
|
||||||
|
resourceID: "2",
|
||||||
|
},
|
||||||
|
expectedErr: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
store, sql := setupTestEnv(t)
|
||||||
|
|
||||||
|
user, err := sql.CreateUser(context.Background(), models.CreateUserCommand{
|
||||||
|
Login: "user",
|
||||||
|
OrgId: 1,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Seed with permission
|
||||||
|
seeded, err := store.SetUserResourcePermissions(context.Background(), user.OrgId, user.Id, accesscontrol.SetResourcePermissionsCommand{
|
||||||
|
Actions: []string{"datasources:query"},
|
||||||
|
Resource: test.add.resource,
|
||||||
|
ResourceID: test.add.resourceID,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = store.RemoveResourcePermission(context.Background(), user.OrgId, accesscontrol.RemoveResourcePermissionCommand{
|
||||||
|
Actions: []string{"datasources:query"},
|
||||||
|
Resource: test.remove.resource,
|
||||||
|
ResourceID: test.remove.resourceID,
|
||||||
|
PermissionID: seeded[0].ID,
|
||||||
|
})
|
||||||
|
|
||||||
|
if test.expectedErr != nil {
|
||||||
|
assert.ErrorIs(t, err, test.expectedErr)
|
||||||
|
} else {
|
||||||
|
permissions, err := store.GetResourcesPermissions(context.Background(), user.OrgId, accesscontrol.GetResourcesPermissionsQuery{
|
||||||
|
Actions: []string{"datasources:query"},
|
||||||
|
Resource: test.add.resource,
|
||||||
|
ResourceIDs: []string{test.add.resourceID},
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if test.add.resourceID != test.remove.resourceID {
|
||||||
|
assert.Len(t, permissions, 1)
|
||||||
|
} else {
|
||||||
|
assert.Len(t, permissions, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type getResourcesPermissionsTest struct {
|
||||||
|
desc string
|
||||||
|
numUsers int
|
||||||
|
actions []string
|
||||||
|
resource string
|
||||||
|
resourceIDs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccessControlStore_GetResourcesPermissions(t *testing.T) {
|
||||||
|
tests := []getResourcesPermissionsTest{
|
||||||
|
{
|
||||||
|
desc: "should return permissions for all resource ids",
|
||||||
|
numUsers: 3,
|
||||||
|
actions: []string{"datasources:query"},
|
||||||
|
resource: "datasources",
|
||||||
|
resourceIDs: []string{"1", "2"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
store, sql := setupTestEnv(t)
|
||||||
|
|
||||||
|
for _, id := range test.resourceIDs {
|
||||||
|
seedResourcePermissions(t, store, sql, test.actions, test.resource, id, test.numUsers)
|
||||||
|
}
|
||||||
|
|
||||||
|
permissions, err := store.GetResourcesPermissions(context.Background(), 1, accesscontrol.GetResourcesPermissionsQuery{
|
||||||
|
Actions: test.actions,
|
||||||
|
Resource: test.resource,
|
||||||
|
ResourceIDs: test.resourceIDs,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expectedLen := test.numUsers * len(test.resourceIDs)
|
||||||
|
assert.Len(t, permissions, expectedLen)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func seedResourcePermissions(t *testing.T, store *AccessControlStore, sql *sqlstore.SQLStore, actions []string, resource, resourceID string, numUsers int) {
|
||||||
|
t.Helper()
|
||||||
|
for i := 0; i < numUsers; i++ {
|
||||||
|
org, _ := sql.GetOrgByName("test")
|
||||||
|
|
||||||
|
if org == nil {
|
||||||
|
addedOrg, err := sql.CreateOrgWithMember("test", int64(i))
|
||||||
|
require.NoError(t, err)
|
||||||
|
org = &addedOrg
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := sql.CreateUser(context.Background(), models.CreateUserCommand{
|
||||||
|
Login: fmt.Sprintf("user:%s%d", resourceID, i),
|
||||||
|
OrgId: org.Id,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = store.SetUserResourcePermissions(context.Background(), 1, u.Id, accesscontrol.SetResourcePermissionsCommand{
|
||||||
|
Actions: actions,
|
||||||
|
Resource: resource,
|
||||||
|
ResourceID: resourceID,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
}
|
26
pkg/services/accesscontrol/database/uid.go
Normal file
26
pkg/services/accesscontrol/database/uid.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
|
"github.com/grafana/grafana/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func generateNewRoleUID(sess *sqlstore.DBSession, orgID int64) (string, error) {
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
uid := util.GenerateShortUID()
|
||||||
|
|
||||||
|
exists, err := sess.Where("org_id=? AND uid=?", orgID, uid).Get(&accesscontrol.Role{})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return uid, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("failed to generate uid")
|
||||||
|
}
|
@ -125,6 +125,34 @@ func fallbackDisplayName(rName string) string {
|
|||||||
return strings.TrimSpace(strings.Replace(rNameWithoutPrefix, ":", " ", -1))
|
return strings.TrimSpace(strings.Replace(rNameWithoutPrefix, ":", " ", -1))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TeamRole struct {
|
||||||
|
ID int64 `json:"id" xorm:"pk autoincr 'id'"`
|
||||||
|
OrgID int64 `json:"orgId" xorm:"org_id"`
|
||||||
|
RoleID int64 `json:"roleId" xorm:"role_id"`
|
||||||
|
TeamID int64 `json:"teamId" xorm:"team_id"`
|
||||||
|
|
||||||
|
Created time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserRole struct {
|
||||||
|
ID int64 `json:"id" xorm:"pk autoincr 'id'"`
|
||||||
|
OrgID int64 `json:"orgId" xorm:"org_id"`
|
||||||
|
RoleID int64 `json:"roleId" xorm:"role_id"`
|
||||||
|
UserID int64 `json:"userId" xorm:"user_id"`
|
||||||
|
|
||||||
|
Created time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type BuiltinRole struct {
|
||||||
|
ID int64 `json:"id" xorm:"pk autoincr 'id'"`
|
||||||
|
RoleID int64 `json:"roleId" xorm:"role_id"`
|
||||||
|
OrgID int64 `json:"orgId" xorm:"org_id"`
|
||||||
|
Role string
|
||||||
|
|
||||||
|
Updated time.Time
|
||||||
|
Created time.Time
|
||||||
|
}
|
||||||
|
|
||||||
// Permission is the model for access control permissions.
|
// Permission is the model for access control permissions.
|
||||||
type Permission struct {
|
type Permission struct {
|
||||||
ID int64 `json:"-" xorm:"pk autoincr 'id'"`
|
ID int64 `json:"-" xorm:"pk autoincr 'id'"`
|
||||||
@ -143,12 +171,58 @@ func (p Permission) OSSPermission() Permission {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GetUserPermissionsQuery struct {
|
||||||
|
OrgID int64 `json:"-"`
|
||||||
|
UserID int64 `json:"userId"`
|
||||||
|
Roles []string
|
||||||
|
}
|
||||||
|
|
||||||
// ScopeParams holds the parameters used to fill in scope templates
|
// ScopeParams holds the parameters used to fill in scope templates
|
||||||
type ScopeParams struct {
|
type ScopeParams struct {
|
||||||
OrgID int64
|
OrgID int64
|
||||||
URLParams map[string]string
|
URLParams map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ResourcePermission 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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ResourcePermission) Managed() bool {
|
||||||
|
return strings.HasPrefix(p.RoleName, "managed:")
|
||||||
|
}
|
||||||
|
|
||||||
|
type SetResourcePermissionsCommand struct {
|
||||||
|
Actions []string
|
||||||
|
Resource string
|
||||||
|
ResourceID string
|
||||||
|
}
|
||||||
|
|
||||||
|
type RemoveResourcePermissionCommand struct {
|
||||||
|
Resource string
|
||||||
|
Actions []string
|
||||||
|
ResourceID string
|
||||||
|
PermissionID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetResourcesPermissionsQuery struct {
|
||||||
|
Actions []string
|
||||||
|
Resource string
|
||||||
|
ResourceIDs []string
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
GlobalOrgID = 0
|
GlobalOrgID = 0
|
||||||
// Permission actions
|
// Permission actions
|
||||||
|
109
pkg/services/accesscontrol/resource_manager.go
Normal file
109
pkg/services/accesscontrol/resource_manager.go
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
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
|
||||||
|
}
|
161
pkg/services/sqlstore/migrations/accesscontrol/migrations.go
Normal file
161
pkg/services/sqlstore/migrations/accesscontrol/migrations.go
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
package accesscontrol
|
||||||
|
|
||||||
|
import "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||||
|
|
||||||
|
func AddMigration(mg *migrator.Migrator) {
|
||||||
|
permissionV1 := migrator.Table{
|
||||||
|
Name: "permission",
|
||||||
|
Columns: []*migrator.Column{
|
||||||
|
{Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||||
|
{Name: "role_id", Type: migrator.DB_BigInt},
|
||||||
|
{Name: "action", Type: migrator.DB_Varchar, Length: 190, Nullable: false},
|
||||||
|
{Name: "scope", Type: migrator.DB_Varchar, Length: 190, Nullable: false},
|
||||||
|
{Name: "created", Type: migrator.DB_DateTime, Nullable: false},
|
||||||
|
{Name: "updated", Type: migrator.DB_DateTime, Nullable: false},
|
||||||
|
},
|
||||||
|
Indices: []*migrator.Index{
|
||||||
|
{Cols: []string{"role_id"}},
|
||||||
|
{Cols: []string{"role_id", "action", "scope"}, Type: migrator.UniqueIndex},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mg.AddMigration("create permission table", migrator.NewAddTableMigration(permissionV1))
|
||||||
|
|
||||||
|
//------- indexes ------------------
|
||||||
|
mg.AddMigration("add unique index permission.role_id", migrator.NewAddIndexMigration(permissionV1, permissionV1.Indices[0]))
|
||||||
|
mg.AddMigration("add unique index role_id_action_scope", migrator.NewAddIndexMigration(permissionV1, permissionV1.Indices[1]))
|
||||||
|
|
||||||
|
roleV1 := migrator.Table{
|
||||||
|
Name: "role",
|
||||||
|
Columns: []*migrator.Column{
|
||||||
|
{Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||||
|
{Name: "name", Type: migrator.DB_NVarchar, Length: 190, Nullable: false},
|
||||||
|
{Name: "description", Type: migrator.DB_Text, Nullable: true},
|
||||||
|
{Name: "version", Type: migrator.DB_BigInt, Nullable: false},
|
||||||
|
{Name: "org_id", Type: migrator.DB_BigInt},
|
||||||
|
{Name: "uid", Type: migrator.DB_NVarchar, Length: 40, Nullable: false},
|
||||||
|
{Name: "created", Type: migrator.DB_DateTime, Nullable: false},
|
||||||
|
{Name: "updated", Type: migrator.DB_DateTime, Nullable: false},
|
||||||
|
},
|
||||||
|
Indices: []*migrator.Index{
|
||||||
|
{Cols: []string{"org_id"}},
|
||||||
|
{Cols: []string{"org_id", "name"}, Type: migrator.UniqueIndex},
|
||||||
|
{Cols: []string{"org_id", "uid"}, Type: migrator.UniqueIndex},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mg.AddMigration("create role table", migrator.NewAddTableMigration(roleV1))
|
||||||
|
|
||||||
|
mg.AddMigration("add column display_name", migrator.NewAddColumnMigration(roleV1, &migrator.Column{
|
||||||
|
Name: "display_name", Type: migrator.DB_NVarchar, Length: 190, Nullable: true,
|
||||||
|
}))
|
||||||
|
//------- indexes ------------------
|
||||||
|
mg.AddMigration("add index role.org_id", migrator.NewAddIndexMigration(roleV1, roleV1.Indices[0]))
|
||||||
|
mg.AddMigration("add unique index role_org_id_name", migrator.NewAddIndexMigration(roleV1, roleV1.Indices[1]))
|
||||||
|
mg.AddMigration("add index role_org_id_uid", migrator.NewAddIndexMigration(roleV1, roleV1.Indices[2]))
|
||||||
|
|
||||||
|
teamRoleV1 := migrator.Table{
|
||||||
|
Name: "team_role",
|
||||||
|
Columns: []*migrator.Column{
|
||||||
|
{Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||||
|
{Name: "org_id", Type: migrator.DB_BigInt},
|
||||||
|
{Name: "team_id", Type: migrator.DB_BigInt},
|
||||||
|
{Name: "role_id", Type: migrator.DB_BigInt},
|
||||||
|
{Name: "created", Type: migrator.DB_DateTime, Nullable: false},
|
||||||
|
},
|
||||||
|
Indices: []*migrator.Index{
|
||||||
|
{Cols: []string{"org_id"}},
|
||||||
|
{Cols: []string{"org_id", "team_id", "role_id"}, Type: migrator.UniqueIndex},
|
||||||
|
{Cols: []string{"team_id"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mg.AddMigration("create team role table", migrator.NewAddTableMigration(teamRoleV1))
|
||||||
|
|
||||||
|
//------- indexes ------------------
|
||||||
|
mg.AddMigration("add index team_role.org_id", migrator.NewAddIndexMigration(teamRoleV1, teamRoleV1.Indices[0]))
|
||||||
|
mg.AddMigration("add unique index team_role_org_id_team_id_role_id", migrator.NewAddIndexMigration(teamRoleV1, teamRoleV1.Indices[1]))
|
||||||
|
mg.AddMigration("add index team_role.team_id", migrator.NewAddIndexMigration(teamRoleV1, teamRoleV1.Indices[2]))
|
||||||
|
|
||||||
|
userRoleV1 := migrator.Table{
|
||||||
|
Name: "user_role",
|
||||||
|
Columns: []*migrator.Column{
|
||||||
|
{Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||||
|
{Name: "org_id", Type: migrator.DB_BigInt},
|
||||||
|
{Name: "user_id", Type: migrator.DB_BigInt},
|
||||||
|
{Name: "role_id", Type: migrator.DB_BigInt},
|
||||||
|
{Name: "created", Type: migrator.DB_DateTime, Nullable: false},
|
||||||
|
},
|
||||||
|
Indices: []*migrator.Index{
|
||||||
|
{Cols: []string{"org_id"}},
|
||||||
|
{Cols: []string{"org_id", "user_id", "role_id"}, Type: migrator.UniqueIndex},
|
||||||
|
{Cols: []string{"user_id"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mg.AddMigration("create user role table", migrator.NewAddTableMigration(userRoleV1))
|
||||||
|
|
||||||
|
//------- indexes ------------------
|
||||||
|
mg.AddMigration("add index user_role.org_id", migrator.NewAddIndexMigration(userRoleV1, userRoleV1.Indices[0]))
|
||||||
|
mg.AddMigration("add unique index user_role_org_id_user_id_role_id", migrator.NewAddIndexMigration(userRoleV1, userRoleV1.Indices[1]))
|
||||||
|
mg.AddMigration("add index user_role.user_id", migrator.NewAddIndexMigration(userRoleV1, userRoleV1.Indices[2]))
|
||||||
|
|
||||||
|
builtinRoleV1 := migrator.Table{
|
||||||
|
Name: "builtin_role",
|
||||||
|
Columns: []*migrator.Column{
|
||||||
|
{Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||||
|
{Name: "role", Type: migrator.DB_NVarchar, Length: 190, Nullable: false},
|
||||||
|
{Name: "role_id", Type: migrator.DB_BigInt},
|
||||||
|
{Name: "created", Type: migrator.DB_DateTime, Nullable: false},
|
||||||
|
{Name: "updated", Type: migrator.DB_DateTime, Nullable: false},
|
||||||
|
},
|
||||||
|
Indices: []*migrator.Index{
|
||||||
|
{Cols: []string{"role_id"}},
|
||||||
|
{Cols: []string{"role"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mg.AddMigration("create builtin role table", migrator.NewAddTableMigration(builtinRoleV1))
|
||||||
|
|
||||||
|
//------- indexes ------------------
|
||||||
|
mg.AddMigration("add index builtin_role.role_id", migrator.NewAddIndexMigration(builtinRoleV1, builtinRoleV1.Indices[0]))
|
||||||
|
mg.AddMigration("add index builtin_role.name", migrator.NewAddIndexMigration(builtinRoleV1, builtinRoleV1.Indices[1]))
|
||||||
|
|
||||||
|
// Add org_id column to the builtin_role table
|
||||||
|
mg.AddMigration("Add column org_id to builtin_role table", migrator.NewAddColumnMigration(builtinRoleV1, &migrator.Column{
|
||||||
|
Name: "org_id", Type: migrator.DB_BigInt, Default: "0",
|
||||||
|
}))
|
||||||
|
|
||||||
|
mg.AddMigration("add index builtin_role.org_id", migrator.NewAddIndexMigration(builtinRoleV1, &migrator.Index{
|
||||||
|
Cols: []string{"org_id"},
|
||||||
|
}))
|
||||||
|
|
||||||
|
mg.AddMigration("add unique index builtin_role_org_id_role_id_role", migrator.NewAddIndexMigration(builtinRoleV1, &migrator.Index{
|
||||||
|
Cols: []string{"org_id", "role_id", "role"}, Type: migrator.UniqueIndex,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Make role.uid unique across Grafana instance
|
||||||
|
mg.AddMigration("Remove unique index role_org_id_uid", migrator.NewDropIndexMigration(roleV1, &migrator.Index{
|
||||||
|
Cols: []string{"org_id", "uid"}, Type: migrator.UniqueIndex,
|
||||||
|
}))
|
||||||
|
|
||||||
|
mg.AddMigration("add unique index role.uid", migrator.NewAddIndexMigration(roleV1, &migrator.Index{
|
||||||
|
Cols: []string{"uid"}, Type: migrator.UniqueIndex,
|
||||||
|
}))
|
||||||
|
|
||||||
|
seedAssignmentV1 := migrator.Table{
|
||||||
|
Name: "seed_assignment",
|
||||||
|
Columns: []*migrator.Column{
|
||||||
|
{Name: "builtin_role", Type: migrator.DB_NVarchar, Length: 190, Nullable: false},
|
||||||
|
{Name: "role_name", Type: migrator.DB_NVarchar, Length: 190, Nullable: false},
|
||||||
|
},
|
||||||
|
Indices: []*migrator.Index{
|
||||||
|
{Cols: []string{"builtin_role", "role_name"}, Type: migrator.UniqueIndex},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mg.AddMigration("create seed assignment table", migrator.NewAddTableMigration(seedAssignmentV1))
|
||||||
|
|
||||||
|
//------- indexes ------------------
|
||||||
|
mg.AddMigration("add unique index builtin_role_role_name", migrator.NewAddIndexMigration(seedAssignmentV1, seedAssignmentV1.Indices[0]))
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package migrations
|
package migrations
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore/migrations/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrations/ualert"
|
"github.com/grafana/grafana/pkg/services/sqlstore/migrations/ualert"
|
||||||
. "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
. "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||||
)
|
)
|
||||||
@ -55,6 +56,7 @@ func (*OSSMigrations) AddMigration(mg *Migrator) {
|
|||||||
addSecretsMigration(mg)
|
addSecretsMigration(mg)
|
||||||
addKVStoreMigrations(mg)
|
addKVStoreMigrations(mg)
|
||||||
ualert.AddDashboardUIDPanelIDMigration(mg)
|
ualert.AddDashboardUIDPanelIDMigration(mg)
|
||||||
|
accesscontrol.AddMigration(mg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addMigrationLogMigrations(mg *Migrator) {
|
func addMigrationLogMigrations(mg *Migrator) {
|
||||||
|
Loading…
Reference in New Issue
Block a user