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:
Karl Persson 2021-11-11 14:02:53 +01:00 committed by GitHub
parent 8c98f24777
commit 3c659f1ea0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1695 additions and 0 deletions

View File

@ -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(

View File

@ -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() {

View 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
}

View 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
}

View 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(&current); 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)
}

View 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
}

View 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)
}
}

View 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")
}

View File

@ -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

View 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
}

View 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]))
}

View File

@ -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) {