Access control: Basic structure and functionality behind feature toggle (#31893)

Co-authored-by: Alexander Zobnin <alexander.zobnin@grafana.com>
Co-authored-by: Emil Tullstedt <emil.tullstedt@grafana.com>
Co-authored-by: Arve Knudsen <arve.knudsen@grafana.com>
Co-authored-by: Marcus Efraimsson <marcus.efraimsson@grafana.com>
pull/32263/head
Alexander Zobnin 4 years ago committed by GitHub
parent fd9dee87e4
commit 20bd591bea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 20
      pkg/api/http_server.go
  2. 11
      pkg/models/org_user.go
  3. 44
      pkg/services/accesscontrol/accesscontrol.go
  4. 57
      pkg/services/accesscontrol/database/common_test.go
  5. 696
      pkg/services/accesscontrol/database/database.go
  6. 65
      pkg/services/accesscontrol/database/database_bench_test.go
  7. 118
      pkg/services/accesscontrol/database/database_mig.go
  8. 387
      pkg/services/accesscontrol/database/database_test.go
  9. 74
      pkg/services/accesscontrol/manager/evaluator.go
  10. 53
      pkg/services/accesscontrol/manager/manager.go
  11. 118
      pkg/services/accesscontrol/manager/manager_test.go
  12. 46
      pkg/services/accesscontrol/middleware/middleware.go
  13. 188
      pkg/services/accesscontrol/models.go
  14. 219
      pkg/services/accesscontrol/seeder/seeder.go
  15. 156
      pkg/services/accesscontrol/seeder/seeder_test.go
  16. 121
      pkg/services/accesscontrol/testing/common.go
  17. 109
      pkg/services/accesscontrol/testing/common_bench.go
  18. 84
      scripts/benchmark-access-control.sh

@ -13,13 +13,9 @@ import (
"strings"
"sync"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/live"
"github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/services/shorturls"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/tsdb"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
macaron "gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/api/routing"
httpstatic "github.com/grafana/grafana/pkg/api/static"
@ -30,24 +26,28 @@ import (
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
_ "github.com/grafana/grafana/pkg/plugins/backendplugin/manager"
"github.com/grafana/grafana/pkg/plugins/plugindashboards"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/contexthandler"
"github.com/grafana/grafana/pkg/services/datasourceproxy"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/hooks"
"github.com/grafana/grafana/pkg/services/librarypanels"
"github.com/grafana/grafana/pkg/services/live"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/provisioning"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/services/shorturls"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb"
"github.com/grafana/grafana/pkg/util/errutil"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
macaron "gopkg.in/macaron.v1"
)
func init() {

@ -38,6 +38,17 @@ func (r RoleType) Includes(other RoleType) bool {
return r == other
}
func (r RoleType) Children() []RoleType {
switch r {
case ROLE_ADMIN:
return []RoleType{ROLE_EDITOR, ROLE_VIEWER}
case ROLE_EDITOR:
return []RoleType{ROLE_VIEWER}
default:
return nil
}
}
func (r *RoleType) UnmarshalJSON(data []byte) error {
var str string
err := json.Unmarshal(data, &str)

@ -0,0 +1,44 @@
package accesscontrol
import (
"context"
"github.com/grafana/grafana/pkg/models"
)
type AccessControl interface {
Evaluator
Store
}
type Evaluator interface {
// Evaluate evaluates access to the given resource
Evaluate(ctx context.Context, user *models.SignedInUser, permission string, scope ...string) (bool, error)
}
type Store interface {
// Database access methods
GetRoles(ctx context.Context, orgID int64) ([]*Role, error)
GetRole(ctx context.Context, orgID, roleID int64) (*RoleDTO, error)
GetRoleByUID(ctx context.Context, orgId int64, uid string) (*RoleDTO, error)
CreateRole(ctx context.Context, cmd CreateRoleCommand) (*Role, error)
CreateRoleWithPermissions(ctx context.Context, cmd CreateRoleWithPermissionsCommand) (*RoleDTO, error)
UpdateRole(ctx context.Context, cmd UpdateRoleCommand) (*RoleDTO, error)
DeleteRole(cmd *DeleteRoleCommand) error
GetRolePermissions(ctx context.Context, roleID int64) ([]Permission, error)
CreatePermission(ctx context.Context, cmd CreatePermissionCommand) (*Permission, error)
UpdatePermission(cmd *UpdatePermissionCommand) (*Permission, error)
DeletePermission(ctx context.Context, cmd *DeletePermissionCommand) error
GetTeamRoles(query *GetTeamRolesQuery) ([]*RoleDTO, error)
GetUserRoles(ctx context.Context, query GetUserRolesQuery) ([]*RoleDTO, error)
GetUserPermissions(ctx context.Context, query GetUserPermissionsQuery) ([]*Permission, error)
AddTeamRole(cmd *AddTeamRoleCommand) error
RemoveTeamRole(cmd *RemoveTeamRoleCommand) error
AddUserRole(cmd *AddUserRoleCommand) error
RemoveUserRole(cmd *RemoveUserRoleCommand) error
AddBuiltinRole(ctx context.Context, orgID, roleID int64, roleName string) error
}
type Seeder interface {
Seed(ctx context.Context, orgID int64) error
}

@ -0,0 +1,57 @@
package database
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/setting"
)
// accessControlStoreTestImpl is a test store implementation which additionally executes a database migrations
type accessControlStoreTestImpl struct {
AccessControlStore
}
func (ac *accessControlStoreTestImpl) AddMigration(mg *migrator.Migrator) {
AddAccessControlMigrations(mg)
}
func setupTestEnv(t testing.TB) *accessControlStoreTestImpl {
t.Helper()
cfg := setting.NewCfg()
store := overrideDatabaseInRegistry(cfg)
sqlStore := sqlstore.InitTestDB(t)
store.SQLStore = sqlStore
err := store.Init()
require.NoError(t, err)
return &store
}
func overrideDatabaseInRegistry(cfg *setting.Cfg) accessControlStoreTestImpl {
store := accessControlStoreTestImpl{
AccessControlStore: AccessControlStore{
SQLStore: nil,
},
}
overrideServiceFunc := func(descriptor registry.Descriptor) (*registry.Descriptor, bool) {
if _, ok := descriptor.Instance.(*AccessControlStore); ok {
return &registry.Descriptor{
Name: "Database",
Instance: &store,
InitPriority: descriptor.InitPriority,
}, true
}
return nil, false
}
registry.RegisterOverride(overrideServiceFunc)
return store
}

@ -0,0 +1,696 @@
package database
import (
"context"
"fmt"
"strings"
"time"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/util"
)
// TimeNow makes it possible to test usage of time
var TimeNow = time.Now
type AccessControlStore struct {
SQLStore *sqlstore.SQLStore `inject:""`
}
func init() {
registry.RegisterService(&AccessControlStore{})
}
func (ac *AccessControlStore) Init() error {
return nil
}
func (ac *AccessControlStore) GetRoles(ctx context.Context, orgID int64) ([]*accesscontrol.Role, error) {
var result []*accesscontrol.Role
err := ac.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
roles := make([]*accesscontrol.Role, 0)
q := "SELECT id, uid, org_id, name, description, updated FROM role WHERE org_id = ?"
if err := sess.SQL(q, orgID).Find(&roles); err != nil {
return err
}
result = roles
return nil
})
return result, err
}
func (ac *AccessControlStore) GetRole(ctx context.Context, orgID, roleID int64) (*accesscontrol.RoleDTO, error) {
var result *accesscontrol.RoleDTO
err := ac.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
role, err := getRoleById(sess, roleID, orgID)
if err != nil {
return err
}
permissions, err := getRolePermissions(sess, roleID)
if err != nil {
return err
}
role.Permissions = permissions
result = role
return nil
})
return result, err
}
func (ac *AccessControlStore) GetRoleByUID(ctx context.Context, orgId int64, uid string) (*accesscontrol.RoleDTO, error) {
var result *accesscontrol.RoleDTO
err := ac.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
role, err := getRoleByUID(sess, uid, orgId)
if err != nil {
return err
}
permissions, err := getRolePermissions(sess, role.ID)
if err != nil {
return err
}
role.Permissions = permissions
result = role
return nil
})
return result, err
}
func (ac *AccessControlStore) CreateRole(ctx context.Context, cmd accesscontrol.CreateRoleCommand) (*accesscontrol.Role, error) {
var result *accesscontrol.Role
err := ac.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
role, err := ac.createRole(sess, cmd)
if err != nil {
return err
}
result = role
return nil
})
return result, err
}
func (ac *AccessControlStore) createRole(sess *sqlstore.DBSession, cmd accesscontrol.CreateRoleCommand) (*accesscontrol.Role, error) {
if cmd.UID == "" {
uid, err := generateNewRoleUID(sess, cmd.OrgID)
if err != nil {
return nil, fmt.Errorf("failed to generate UID for role %q: %w", cmd.Name, err)
}
cmd.UID = uid
}
role := &accesscontrol.Role{
OrgID: cmd.OrgID,
UID: cmd.UID,
Name: cmd.Name,
Description: cmd.Description,
Created: TimeNow(),
Updated: TimeNow(),
}
if _, err := sess.Insert(role); err != nil {
if ac.SQLStore.Dialect.IsUniqueConstraintViolation(err) && strings.Contains(err.Error(), "name") {
return nil, fmt.Errorf("role with the name '%s' already exists: %w", cmd.Name, err)
}
return nil, err
}
return role, nil
}
func (ac *AccessControlStore) CreateRoleWithPermissions(ctx context.Context, cmd accesscontrol.CreateRoleWithPermissionsCommand) (*accesscontrol.RoleDTO, error) {
var result *accesscontrol.RoleDTO
err := ac.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
createRoleCmd := accesscontrol.CreateRoleCommand{
OrgID: cmd.OrgID,
UID: cmd.UID,
Name: cmd.Name,
Description: cmd.Description,
}
role, err := ac.createRole(sess, createRoleCmd)
if err != nil {
return err
}
result = &accesscontrol.RoleDTO{
ID: role.ID,
UID: role.UID,
OrgID: role.OrgID,
Name: role.Name,
Description: role.Description,
Created: role.Created,
Updated: role.Updated,
}
// Add permissions
for _, p := range cmd.Permissions {
createPermissionCmd := accesscontrol.CreatePermissionCommand{
RoleID: role.ID,
Permission: p.Permission,
Scope: p.Scope,
}
permission, err := createPermission(sess, createPermissionCmd)
if err != nil {
return err
}
result.Permissions = append(result.Permissions, *permission)
}
return nil
})
return result, err
}
// UpdateRole updates role with permissions
func (ac *AccessControlStore) UpdateRole(ctx context.Context, cmd accesscontrol.UpdateRoleCommand) (*accesscontrol.RoleDTO, error) {
var result *accesscontrol.RoleDTO
err := ac.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
// TODO: work with both ID and UID
existingRole, err := getRoleByUID(sess, cmd.UID, cmd.OrgID)
if err != nil {
return err
}
version := existingRole.Version + 1
if cmd.Version != 0 {
if existingRole.Version >= cmd.Version {
return fmt.Errorf(
"could not update '%s' (UID %s) from version %d to %d: %w",
cmd.Name,
existingRole.UID,
existingRole.Version,
cmd.Version,
accesscontrol.ErrVersionLE,
)
}
version = cmd.Version
}
role := &accesscontrol.Role{
ID: existingRole.ID,
UID: existingRole.UID,
Version: version,
OrgID: existingRole.OrgID,
Name: cmd.Name,
Description: cmd.Description,
Updated: TimeNow(),
}
affectedRows, err := sess.ID(existingRole.ID).Update(role)
if err != nil {
return err
}
if affectedRows == 0 {
return accesscontrol.ErrRoleNotFound
}
result = &accesscontrol.RoleDTO{
ID: role.ID,
Version: version,
UID: role.UID,
OrgID: role.OrgID,
Name: role.Name,
Description: role.Description,
Created: role.Created,
Updated: role.Updated,
}
// Delete role's permissions
_, err = sess.Exec("DELETE FROM permission WHERE role_id = ?", existingRole.ID)
if err != nil {
return err
}
// Add permissions
for _, p := range cmd.Permissions {
createPermissionCmd := accesscontrol.CreatePermissionCommand{
RoleID: role.ID,
Permission: p.Permission,
Scope: p.Scope,
}
permission, err := createPermission(sess, createPermissionCmd)
if err != nil {
return err
}
result.Permissions = append(result.Permissions, *permission)
}
return nil
})
return result, err
}
func (ac *AccessControlStore) DeleteRole(cmd *accesscontrol.DeleteRoleCommand) error {
return ac.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
roleId := cmd.ID
if roleId == 0 {
role, err := getRoleByUID(sess, cmd.UID, cmd.OrgID)
if err != nil {
return err
}
roleId = role.ID
}
// Delete role's permissions
_, err := sess.Exec("DELETE FROM permission WHERE role_id = ?", roleId)
if err != nil {
return err
}
_, err = sess.Exec("DELETE FROM role WHERE id = ? AND org_id = ?", roleId, cmd.OrgID)
if err != nil {
return err
}
return nil
})
}
func (ac *AccessControlStore) GetRolePermissions(ctx context.Context, roleID int64) ([]accesscontrol.Permission, error) {
var result []accesscontrol.Permission
err := ac.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
permissions, err := getRolePermissions(sess, roleID)
if err != nil {
return err
}
result = permissions
return nil
})
return result, err
}
func (ac *AccessControlStore) CreatePermission(ctx context.Context, cmd accesscontrol.CreatePermissionCommand) (*accesscontrol.Permission, error) {
var result *accesscontrol.Permission
err := ac.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
permission, err := createPermission(sess, cmd)
if err != nil {
return err
}
result = permission
return nil
})
return result, err
}
func (ac *AccessControlStore) UpdatePermission(cmd *accesscontrol.UpdatePermissionCommand) (*accesscontrol.Permission, error) {
var result *accesscontrol.Permission
err := ac.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
permission := &accesscontrol.Permission{
Permission: cmd.Permission,
Scope: cmd.Scope,
Updated: TimeNow(),
}
affectedRows, err := sess.ID(cmd.ID).Update(permission)
if err != nil {
return err
}
if affectedRows == 0 {
return accesscontrol.ErrPermissionNotFound
}
result = permission
return nil
})
return result, err
}
func (ac *AccessControlStore) DeletePermission(ctx context.Context, cmd *accesscontrol.DeletePermissionCommand) error {
return ac.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
_, err := sess.Exec("DELETE FROM permission WHERE id = ?", cmd.ID)
if err != nil {
return err
}
return nil
})
}
func (ac *AccessControlStore) GetTeamRoles(query *accesscontrol.GetTeamRolesQuery) ([]*accesscontrol.RoleDTO, error) {
var result []*accesscontrol.RoleDTO
err := ac.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
q := `SELECT
role.id,
role.name AS name,
role.description AS description,
role.updated FROM role AS role
INNER JOIN team_role ON role.id = team_role.role_id AND team_role.team_id = ?
WHERE role.org_id = ? `
if err := sess.SQL(q, query.TeamID, query.OrgID).Find(&result); err != nil {
return err
}
return nil
})
return result, err
}
func (ac *AccessControlStore) GetUserRoles(ctx context.Context, query accesscontrol.GetUserRolesQuery) ([]*accesscontrol.RoleDTO, error) {
var result []*accesscontrol.RoleDTO
err := ac.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
// TODO: optimize this
filter, params := ac.userRolesFilter(query.OrgID, query.UserID, query.Roles)
q := `SELECT
role.id,
role.org_id,
role.name,
role.description,
role.created,
role.updated
FROM role
` + filter
err := sess.SQL(q, params...).Find(&result)
return err
})
return result, err
}
func (ac *AccessControlStore) GetUserPermissions(ctx context.Context, query accesscontrol.GetUserPermissionsQuery) ([]*accesscontrol.Permission, error) {
var result []*accesscontrol.Permission
err := ac.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
filter, params := ac.userRolesFilter(query.OrgID, query.UserID, query.Roles)
// TODO: optimize this
q := `SELECT
permission.id,
permission.role_id,
permission.permission,
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 (*AccessControlStore) userRolesFilter(orgID, userID int64, roles []string) (string, []interface{}) {
q := `WHERE role.id IN (
SELECT up.role_id FROM user_role AS up WHERE up.user_id = ?
UNION
SELECT tp.role_id FROM team_role as tp
INNER JOIN team_member as tm ON tm.team_id = tp.team_id
WHERE tm.user_id = ?`
params := []interface{}{userID, userID}
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 role.org_id = ?`
params = append(params, orgID)
return q, params
}
func (ac *AccessControlStore) AddTeamRole(cmd *accesscontrol.AddTeamRoleCommand) error {
return ac.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
if res, err := sess.Query("SELECT 1 from team_role WHERE org_id=? and team_id=? and role_id=?", cmd.OrgID, cmd.TeamID, cmd.RoleID); err != nil {
return err
} else if len(res) == 1 {
return accesscontrol.ErrTeamRoleAlreadyAdded
}
if _, err := teamExists(cmd.OrgID, cmd.TeamID, sess); err != nil {
return err
}
if _, err := roleExists(cmd.OrgID, cmd.RoleID, sess); err != nil {
return err
}
teamRole := &accesscontrol.TeamRole{
OrgID: cmd.OrgID,
TeamID: cmd.TeamID,
RoleID: cmd.RoleID,
Created: TimeNow(),
}
_, err := sess.Insert(teamRole)
return err
})
}
func (ac *AccessControlStore) RemoveTeamRole(cmd *accesscontrol.RemoveTeamRoleCommand) error {
return ac.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
if _, err := teamExists(cmd.OrgID, cmd.TeamID, sess); err != nil {
return err
}
if _, err := roleExists(cmd.OrgID, cmd.RoleID, sess); err != nil {
return err
}
q := "DELETE FROM team_role WHERE org_id=? and team_id=? and role_id=?"
res, err := sess.Exec(q, cmd.OrgID, cmd.TeamID, cmd.RoleID)
if err != nil {
return err
}
rows, err := res.RowsAffected()
if rows == 0 {
return accesscontrol.ErrTeamRoleNotFound
}
return err
})
}
func (ac *AccessControlStore) AddUserRole(cmd *accesscontrol.AddUserRoleCommand) error {
return ac.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
if res, err := sess.Query("SELECT 1 from user_role WHERE org_id=? and user_id=? and role_id=?", cmd.OrgID, cmd.UserID, cmd.RoleID); err != nil {
return err
} else if len(res) == 1 {
return accesscontrol.ErrUserRoleAlreadyAdded
}
if _, err := roleExists(cmd.OrgID, cmd.RoleID, sess); err != nil {
return err
}
userRole := &accesscontrol.UserRole{
OrgID: cmd.OrgID,
UserID: cmd.UserID,
RoleID: cmd.RoleID,
Created: TimeNow(),
}
_, err := sess.Insert(userRole)
return err
})
}
func (ac *AccessControlStore) RemoveUserRole(cmd *accesscontrol.RemoveUserRoleCommand) error {
return ac.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
if _, err := roleExists(cmd.OrgID, cmd.RoleID, sess); err != nil {
return err
}
q := "DELETE FROM user_role WHERE org_id=? and user_id=? and role_id=?"
res, err := sess.Exec(q, cmd.OrgID, cmd.UserID, cmd.RoleID)
if err != nil {
return err
}
rows, err := res.RowsAffected()
if rows == 0 {
return accesscontrol.ErrUserRoleNotFound
}
return err
})
}
func (ac *AccessControlStore) AddBuiltinRole(ctx context.Context, orgID, roleID int64, roleName string) error {
if !models.RoleType(roleName).IsValid() && roleName != "Grafana Admin" {
return fmt.Errorf("role '%s' is not a valid role", roleName)
}
return ac.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
if res, err := sess.Query("SELECT 1 from builtin_role WHERE role_id=? and role=?", roleID, roleName); err != nil {
return err
} else if len(res) == 1 {
return accesscontrol.ErrUserRoleAlreadyAdded
}
if _, err := roleExists(orgID, roleID, sess); err != nil {
return err
}
role := accesscontrol.BuiltinRole{
RoleID: roleID,
Role: roleName,
Updated: TimeNow(),
Created: TimeNow(),
}
_, err := sess.Table("builtin_role").Insert(role)
return err
})
}
func getRoleById(sess *sqlstore.DBSession, roleId int64, orgId int64) (*accesscontrol.RoleDTO, error) {
role := accesscontrol.Role{OrgID: orgId, ID: roleId}
has, err := sess.Get(&role)
if !has {
return nil, accesscontrol.ErrRoleNotFound
}
if err != nil {
return nil, err
}
roleDTO := accesscontrol.RoleDTO{
ID: roleId,
OrgID: role.OrgID,
Name: role.Name,
Description: role.Description,
Permissions: nil,
Created: role.Created,
Updated: role.Updated,
}
return &roleDTO, nil
}
func getRoleByUID(sess *sqlstore.DBSession, uid string, orgId int64) (*accesscontrol.RoleDTO, error) {
role := accesscontrol.Role{OrgID: orgId, UID: uid}
has, err := sess.Get(&role)
if !has {
return nil, accesscontrol.ErrRoleNotFound
}
if err != nil {
return nil, err
}
roleDTO := accesscontrol.RoleDTO{
ID: role.ID,
UID: role.UID,
Version: role.Version,
OrgID: role.OrgID,
Name: role.Name,
Description: role.Description,
Permissions: nil,
Created: role.Created,
Updated: role.Updated,
}
return &roleDTO, nil
}
func getRolePermissions(sess *sqlstore.DBSession, roleId int64) ([]accesscontrol.Permission, error) {
permissions := make([]accesscontrol.Permission, 0)
q := "SELECT id, role_id, permission, scope, updated, created FROM permission WHERE role_id = ?"
if err := sess.SQL(q, roleId).Find(&permissions); err != nil {
return nil, err
}
return permissions, nil
}
func createPermission(sess *sqlstore.DBSession, cmd accesscontrol.CreatePermissionCommand) (*accesscontrol.Permission, error) {
permission := &accesscontrol.Permission{
RoleID: cmd.RoleID,
Permission: cmd.Permission,
Scope: cmd.Scope,
Created: TimeNow(),
Updated: TimeNow(),
}
if _, err := sess.Insert(permission); err != nil {
return nil, err
}
return permission, nil
}
func teamExists(orgId int64, teamId int64, sess *sqlstore.DBSession) (bool, error) {
if res, err := sess.Query("SELECT 1 from team WHERE org_id=? and id=?", orgId, teamId); err != nil {
return false, err
} else if len(res) != 1 {
return false, accesscontrol.ErrTeamNotFound
}
return true, nil
}
func roleExists(orgId int64, roleId int64, sess *sqlstore.DBSession) (bool, error) {
if res, err := sess.Query("SELECT 1 from role WHERE org_id=? and id=?", orgId, roleId); err != nil {
return false, err
} else if len(res) != 1 {
return false, accesscontrol.ErrRoleNotFound
}
return true, nil
}
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 "", accesscontrol.ErrRoleFailedGenerateUniqueUID
}
func MockTimeNow() {
var timeSeed int64
TimeNow = func() time.Time {
fakeNow := time.Unix(timeSeed, 0).UTC()
timeSeed++
return fakeNow
}
}
func ResetTimeNow() {
TimeNow = time.Now
}

@ -0,0 +1,65 @@
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/registry"
"github.com/grafana/grafana/pkg/services/accesscontrol"
actesting "github.com/grafana/grafana/pkg/services/accesscontrol/testing"
"github.com/grafana/grafana/pkg/services/sqlstore"
)
func setup(b *testing.B, rolesPerUser, users int) *accessControlStoreTestImpl {
ac := setupTestEnv(b)
b.Cleanup(registry.ClearOverrides)
actesting.GenerateRoles(b, ac.SQLStore, ac, rolesPerUser, users)
return ac
}
func getRoles(b *testing.B, ac accesscontrol.Store, rolesPerUser, users int) {
userQuery := models.GetUserByLoginQuery{
LoginOrEmail: "user1@test.com",
}
err := sqlstore.GetUserByLogin(&userQuery)
require.NoError(b, err)
userId := userQuery.Result.Id
userPermissionsQuery := accesscontrol.GetUserPermissionsQuery{OrgID: 1, UserID: userId}
res, err := ac.GetUserPermissions(context.Background(), userPermissionsQuery)
require.NoError(b, err)
expectedPermissions := actesting.PermissionsPerRole * rolesPerUser
assert.Greater(b, len(res), expectedPermissions)
}
func benchmarkRoles(b *testing.B, rolesPerUser, users int) {
ac := setup(b, rolesPerUser, users)
// We don't wanna measure DB initialization
b.ResetTimer()
for i := 0; i < b.N; i++ {
getRoles(b, ac, rolesPerUser, users)
}
}
func BenchmarkRolesUsers10_10(b *testing.B) { benchmarkRoles(b, 10, 10) }
func BenchmarkRolesUsers10_100(b *testing.B) { benchmarkRoles(b, 10, 100) }
func BenchmarkRolesUsers10_500(b *testing.B) { benchmarkRoles(b, 10, 500) }
func BenchmarkRolesUsers10_1000(b *testing.B) { benchmarkRoles(b, 10, 1000) }
func BenchmarkRolesUsers10_5000(b *testing.B) { benchmarkRoles(b, 10, 5000) }
func BenchmarkRolesUsers10_10000(b *testing.B) {
if testing.Short() {
b.Skip("Skipping benchmark in short mode")
}
benchmarkRoles(b, 10, 10000)
}
func BenchmarkRolesPerUser10_10(b *testing.B) { benchmarkRoles(b, 10, 10) }
func BenchmarkRolesPerUser100_10(b *testing.B) { benchmarkRoles(b, 100, 10) }
func BenchmarkRolesPerUser500_10(b *testing.B) { benchmarkRoles(b, 500, 10) }
func BenchmarkRolesPerUser1000_10(b *testing.B) { benchmarkRoles(b, 1000, 10) }

@ -0,0 +1,118 @@
package database
import "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
func AddAccessControlMigrations(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: "permission", 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"}},
},
}
mg.AddMigration("create permission table", migrator.NewAddTableMigration(permissionV1))
//------- indexes ------------------
mg.AddMigration("add unique index permission.role_id", migrator.NewAddIndexMigration(permissionV1, permissionV1.Indices[0]))
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))
//------- 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]))
}

@ -0,0 +1,387 @@
// +build integration
package database
import (
"context"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/accesscontrol"
actesting "github.com/grafana/grafana/pkg/services/accesscontrol/testing"
"github.com/grafana/grafana/pkg/services/sqlstore"
)
func TestCreatingRole(t *testing.T) {
MockTimeNow()
t.Cleanup(ResetTimeNow)
testCases := []struct {
desc string
role actesting.RoleTestCase
permissions []actesting.PermissionTestCase
expectedError error
expectedUpdated time.Time
}{
{
desc: "should successfully create simple role",
role: actesting.RoleTestCase{
Name: "a name",
Permissions: nil,
},
expectedUpdated: time.Unix(1, 0).UTC(),
},
{
desc: "should successfully create role with UID",
role: actesting.RoleTestCase{
Name: "a name",
UID: "testUID",
Permissions: nil,
},
expectedUpdated: time.Unix(3, 0).UTC(),
},
{
desc: "should successfully create role with permissions",
role: actesting.RoleTestCase{
Name: "a name",
Permissions: []actesting.PermissionTestCase{
{Scope: "users", Permission: "admin.users:create"},
{Scope: "reports", Permission: "reports:read"},
},
},
expectedUpdated: time.Unix(5, 0).UTC(),
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
store := setupTestEnv(t)
t.Cleanup(registry.ClearOverrides)
createRoleRes := actesting.CreateRole(t, store, tc.role)
res, err := store.GetRoleByUID(context.Background(), 1, createRoleRes.UID)
role := res
require.NoError(t, err)
assert.Equal(t, tc.expectedUpdated, role.Updated)
if tc.role.UID != "" {
assert.Equal(t, tc.role.UID, role.UID)
}
if tc.role.Permissions == nil {
assert.Empty(t, role.Permissions)
} else {
assert.Len(t, tc.role.Permissions, len(role.Permissions))
for i, p := range role.Permissions {
assert.Equal(t, tc.role.Permissions[i].Permission, p.Permission)
assert.Equal(t, tc.role.Permissions[i].Scope, p.Scope)
}
}
})
}
}
func TestUpdatingRole(t *testing.T) {
MockTimeNow()
t.Cleanup(ResetTimeNow)
testCases := []struct {
desc string
role actesting.RoleTestCase
newRole actesting.RoleTestCase
expectedError error
}{
{
desc: "should successfully update role name",
role: actesting.RoleTestCase{
Name: "a name",
Permissions: []actesting.PermissionTestCase{
{Scope: "reports", Permission: "reports:read"},
},
},
newRole: actesting.RoleTestCase{
Name: "a different name",
Permissions: []actesting.PermissionTestCase{
{Scope: "reports", Permission: "reports:create"},
{Scope: "reports", Permission: "reports:read"},
},
},
},
{
desc: "should successfully create role with permissions",
role: actesting.RoleTestCase{
Name: "a name",
Permissions: []actesting.PermissionTestCase{
{Scope: "users", Permission: "admin.users:create"},
{Scope: "reports", Permission: "reports:read"},
},
},
newRole: actesting.RoleTestCase{
Name: "a different name",
Permissions: []actesting.PermissionTestCase{
{Scope: "users", Permission: "admin.users:read"},
{Scope: "reports", Permission: "reports:create"},
},
},
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
store := setupTestEnv(t)
t.Cleanup(registry.ClearOverrides)
role := actesting.CreateRole(t, store, tc.role)
updated := role.Updated
updateRoleCmd := accesscontrol.UpdateRoleCommand{
UID: role.UID,
Name: tc.newRole.Name,
}
for _, perm := range tc.newRole.Permissions {
updateRoleCmd.Permissions = append(updateRoleCmd.Permissions, accesscontrol.Permission{
Permission: perm.Permission,
Scope: perm.Scope,
})
}
_, err := store.UpdateRole(context.Background(), updateRoleCmd)
require.NoError(t, err)
updatedRole, err := store.GetRoleByUID(context.Background(), 1, role.UID)
require.NoError(t, err)
assert.Equal(t, tc.newRole.Name, updatedRole.Name)
assert.True(t, updatedRole.Updated.After(updated))
assert.Equal(t, len(tc.newRole.Permissions), len(updatedRole.Permissions))
// Check permissions
require.NoError(t, err)
for i, updatedPermission := range updatedRole.Permissions {
assert.Equal(t, tc.newRole.Permissions[i].Permission, updatedPermission.Permission)
assert.Equal(t, tc.newRole.Permissions[i].Scope, updatedPermission.Scope)
}
})
}
}
type userRoleTestCase struct {
desc string
userName string
teamName string
userRoles []actesting.RoleTestCase
teamRoles []actesting.RoleTestCase
}
func TestUserRole(t *testing.T) {
MockTimeNow()
t.Cleanup(ResetTimeNow)
testCases := []userRoleTestCase{
{
desc: "should successfully get user roles",
userName: "testuser",
teamName: "team1",
userRoles: []actesting.RoleTestCase{
{
Name: "CreateUser", Permissions: []actesting.PermissionTestCase{},
},
},
teamRoles: nil,
},
{
desc: "should successfully get user and team roles",
userName: "testuser",
teamName: "team1",
userRoles: []actesting.RoleTestCase{
{
Name: "CreateUser", Permissions: []actesting.PermissionTestCase{},
},
},
teamRoles: []actesting.RoleTestCase{
{
Name: "CreateDataSource", Permissions: []actesting.PermissionTestCase{},
},
{
Name: "EditDataSource", Permissions: []actesting.PermissionTestCase{},
},
},
},
{
desc: "should successfully get user and team roles if user has no roles",
userName: "testuser",
teamName: "team1",
userRoles: nil,
teamRoles: []actesting.RoleTestCase{
{
Name: "CreateDataSource", Permissions: []actesting.PermissionTestCase{},
},
{
Name: "EditDataSource", Permissions: []actesting.PermissionTestCase{},
},
},
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
store := setupTestEnv(t)
t.Cleanup(registry.ClearOverrides)
actesting.CreateUserWithRole(t, store.SQLStore, store, tc.userName, tc.userRoles)
actesting.CreateTeamWithRole(t, store.SQLStore, store, tc.teamName, tc.teamRoles)
// Create more teams
for i := 0; i < 10; i++ {
teamName := fmt.Sprintf("faketeam%v", i)
roles := []actesting.RoleTestCase{
{
Name: fmt.Sprintf("fakerole%v", i),
Permissions: []actesting.PermissionTestCase{
{Scope: "datasources", Permission: "datasources:create"},
},
},
}
actesting.CreateTeamWithRole(t, store.SQLStore, store, teamName, roles)
}
userQuery := models.GetUserByLoginQuery{
LoginOrEmail: tc.userName,
}
err := sqlstore.GetUserByLogin(&userQuery)
require.NoError(t, err)
userId := userQuery.Result.Id
teamQuery := models.SearchTeamsQuery{
OrgId: 1,
Name: tc.teamName,
}
err = sqlstore.SearchTeams(&teamQuery)
require.NoError(t, err)
require.Len(t, teamQuery.Result.Teams, 1)
teamId := teamQuery.Result.Teams[0].Id
err = store.SQLStore.AddTeamMember(userId, 1, teamId, false, 1)
require.NoError(t, err)
userRolesQuery := accesscontrol.GetUserRolesQuery{
OrgID: 1,
UserID: userQuery.Result.Id,
}
res, err := store.GetUserRoles(context.Background(), userRolesQuery)
require.NoError(t, err)
assert.Equal(t, len(tc.userRoles)+len(tc.teamRoles), len(res))
})
}
}
type userTeamRoleTestCase struct {
desc string
userName string
teamName string
userRoles []actesting.RoleTestCase
teamRoles []actesting.RoleTestCase
}
func TestUserPermissions(t *testing.T) {
MockTimeNow()
t.Cleanup(ResetTimeNow)
testCases := []userTeamRoleTestCase{
{
desc: "should successfully get user and team permissions",
userName: "testuser",
teamName: "team1",
userRoles: []actesting.RoleTestCase{
{
Name: "CreateUser", Permissions: []actesting.PermissionTestCase{
{Scope: "users", Permission: "admin.users:create"},
{Scope: "reports", Permission: "reports:read"},
},
},
},
teamRoles: []actesting.RoleTestCase{
{
Name: "CreateDataSource", Permissions: []actesting.PermissionTestCase{
{Scope: "datasources", Permission: "datasources:create"},
},
},
},
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
store := setupTestEnv(t)
t.Cleanup(registry.ClearOverrides)
actesting.CreateUserWithRole(t, store.SQLStore, store, tc.userName, tc.userRoles)
actesting.CreateTeamWithRole(t, store.SQLStore, store, tc.teamName, tc.teamRoles)
// Create more teams
for i := 0; i < 10; i++ {
teamName := fmt.Sprintf("faketeam%v", i)
roles := []actesting.RoleTestCase{
{
Name: fmt.Sprintf("fakerole%v", i),
Permissions: []actesting.PermissionTestCase{
{Scope: "datasources", Permission: "datasources:create"},
},
},
}
actesting.CreateTeamWithRole(t, store.SQLStore, store, teamName, roles)
}
userQuery := models.GetUserByLoginQuery{
LoginOrEmail: tc.userName,
}
err := sqlstore.GetUserByLogin(&userQuery)
require.NoError(t, err)
userId := userQuery.Result.Id
teamQuery := models.SearchTeamsQuery{
OrgId: 1,
Name: tc.teamName,
}
err = sqlstore.SearchTeams(&teamQuery)
require.NoError(t, err)
require.Len(t, teamQuery.Result.Teams, 1)
teamId := teamQuery.Result.Teams[0].Id
err = store.SQLStore.AddTeamMember(userId, 1, teamId, false, 1)
require.NoError(t, err)
userPermissionsQuery := accesscontrol.GetUserPermissionsQuery{
OrgID: 1,
UserID: userId,
}
getUserTeamsQuery := models.GetTeamsByUserQuery{
OrgId: 1,
UserId: userId,
}
err = sqlstore.GetTeamsByUser(&getUserTeamsQuery)
require.NoError(t, err)
require.Len(t, getUserTeamsQuery.Result, 1)
expectedPermissions := []actesting.PermissionTestCase{}
for _, p := range tc.userRoles {
expectedPermissions = append(expectedPermissions, p.Permissions...)
}
for _, p := range tc.teamRoles {
expectedPermissions = append(expectedPermissions, p.Permissions...)
}
res, err := store.GetUserPermissions(context.Background(), userPermissionsQuery)
require.NoError(t, err)
assert.Len(t, res, len(expectedPermissions))
assert.Contains(t, expectedPermissions, actesting.PermissionTestCase{Scope: "datasources", Permission: "datasources:create"})
assert.NotContains(t, expectedPermissions, actesting.PermissionTestCase{Scope: "/api/restricted", Permission: "restricted:read"})
})
}
}

@ -0,0 +1,74 @@
package manager
import (
"context"
"github.com/gobwas/glob"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
)
const roleGrafanaAdmin = "Grafana Admin"
func (m *Manager) Evaluate(ctx context.Context, user *models.SignedInUser, permission string, scope ...string) (bool, error) {
roles := []string{string(user.OrgRole)}
for _, role := range user.OrgRole.Children() {
roles = append(roles, string(role))
}
if user.IsGrafanaAdmin {
roles = append(roles, roleGrafanaAdmin)
}
res, err := m.GetUserPermissions(ctx, accesscontrol.GetUserPermissionsQuery{
OrgID: user.OrgId,
UserID: user.UserId,
Roles: roles,
})
if err != nil {
return false, err
}
ok, dbScopes := extractPermission(res, permission)
if !ok {
return false, nil
}
for _, s := range scope {
var match bool
for dbScope := range dbScopes {
rule, err := glob.Compile(dbScope, ':', '/')
if err != nil {
return false, err
}
match = rule.Match(s)
if match {
break
}
}
if !match {
return false, nil
}
}
return true, nil
}
func extractPermission(permissions []*accesscontrol.Permission, permission string) (bool, map[string]struct{}) {
scopes := map[string]struct{}{}
ok := false
for _, p := range permissions {
if p == nil {
continue
}
if p.Permission == permission {
ok = true
scopes[p.Scope] = struct{}{}
}
}
return ok, scopes
}

@ -0,0 +1,53 @@
package manager
import (
"context"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
"github.com/grafana/grafana/pkg/services/accesscontrol/seeder"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/setting"
)
// Manager is the service implementing role based access control.
type Manager struct {
Cfg *setting.Cfg `inject:""`
RouteRegister routing.RouteRegister `inject:""`
Log log.Logger
*database.AccessControlStore
}
func init() {
registry.RegisterService(&Manager{})
}
// Init initializes the Manager.
func (m *Manager) Init() error {
m.Log = log.New("accesscontrol")
seeder := seeder.NewSeeder(m, m.Log)
// TODO: Seed all orgs
err := seeder.Seed(context.TODO(), 1)
if err != nil {
return err
}
return nil
}
func (m *Manager) IsDisabled() bool {
_, exists := m.Cfg.FeatureToggles["accesscontrol"]
return !exists
}
func (m *Manager) AddMigration(mg *migrator.Migrator) {
if m.IsDisabled() {
return
}
database.AddAccessControlMigrations(mg)
}

@ -0,0 +1,118 @@
package manager
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
actesting "github.com/grafana/grafana/pkg/services/accesscontrol/testing"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
)
func setupTestEnv(t testing.TB) *Manager {
t.Helper()
cfg := setting.NewCfg()
cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
ac := overrideAccessControlInRegistry(t, cfg)
sqlStore := sqlstore.InitTestDB(t)
ac.AccessControlStore.SQLStore = sqlStore
err := ac.Init()
require.NoError(t, err)
return &ac
}
func overrideAccessControlInRegistry(t testing.TB, cfg *setting.Cfg) Manager {
t.Helper()
ac := Manager{
Cfg: cfg,
RouteRegister: routing.NewRouteRegister(),
Log: log.New("accesscontrol-test"),
AccessControlStore: &database.AccessControlStore{
SQLStore: nil,
},
}
overrideServiceFunc := func(descriptor registry.Descriptor) (*registry.Descriptor, bool) {
if _, ok := descriptor.Instance.(*Manager); ok {
return &registry.Descriptor{
Name: "AccessControl",
Instance: &ac,
InitPriority: descriptor.InitPriority,
}, true
}
return nil, false
}
registry.RegisterOverride(overrideServiceFunc)
return ac
}
type evaluatingPermissionsTestCase struct {
desc string
userName string
roles []actesting.RoleTestCase
}
func TestEvaluatingPermissions(t *testing.T) {
testCases := []evaluatingPermissionsTestCase{
{
desc: "should successfully evaluate access to the endpoint",
userName: "testuser",
roles: []actesting.RoleTestCase{
{
Name: "CreateUser", Permissions: []actesting.PermissionTestCase{
{Scope: "/api/admin/users", Permission: "post"},
{Scope: "/api/report", Permission: "get"},
},
},
},
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
ac := setupTestEnv(t)
t.Cleanup(registry.ClearOverrides)
actesting.CreateUserWithRole(t, ac.SQLStore, ac, tc.userName, tc.roles)
userQuery := models.GetUserByLoginQuery{
LoginOrEmail: tc.userName,
}
err := sqlstore.GetUserByLogin(&userQuery)
require.NoError(t, err)
userRolesQuery := accesscontrol.GetUserRolesQuery{
OrgID: 1,
UserID: userQuery.Result.Id,
}
res, err := ac.GetUserRoles(context.Background(), userRolesQuery)
require.NoError(t, err)
assert.Len(t, res, len(tc.roles))
userPermissionsQuery := accesscontrol.GetUserPermissionsQuery{
OrgID: 1,
UserID: userQuery.Result.Id,
}
permissions, err := ac.GetUserPermissions(context.Background(), userPermissionsQuery)
require.NoError(t, err)
assert.Len(t, permissions, len(tc.roles[0].Permissions))
})
}
}

@ -0,0 +1,46 @@
package middleware
import (
"bytes"
"net/http"
"text/template"
macaron "gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
)
func Middleware(ac accesscontrol.AccessControl) func(string, ...string) macaron.Handler {
return func(permission string, scopes ...string) macaron.Handler {
return func(c *models.ReqContext) {
for i, scope := range scopes {
var buf bytes.Buffer
tmpl, err := template.New("scope").Parse(scope)
if err != nil {
c.JsonApiErr(http.StatusInternalServerError, "Internal server error", err)
return
}
err = tmpl.Execute(&buf, c.AllParams())
if err != nil {
c.JsonApiErr(http.StatusInternalServerError, "Internal server error", err)
return
}
scopes[i] = buf.String()
}
hasAccess, err := ac.Evaluate(c.Req.Context(), c.SignedInUser, permission, scopes...)
if err != nil {
c.Logger.Error("Error from access control system", "error", err)
c.JsonApiErr(http.StatusForbidden, "Forbidden", nil)
return
}
if !hasAccess {
c.Logger.Info("Access denied", "error", err, "userID", c.UserId, "permission", permission, "scopes", scopes)
c.JsonApiErr(http.StatusForbidden, "Forbidden", nil)
return
}
}
}
}

@ -0,0 +1,188 @@
package accesscontrol
import (
"errors"
"time"
)
var (
ErrRoleNotFound = errors.New("role not found")
ErrTeamRoleAlreadyAdded = errors.New("role is already added to this team")
ErrUserRoleAlreadyAdded = errors.New("role is already added to this user")
ErrTeamRoleNotFound = errors.New("team role not found")
ErrUserRoleNotFound = errors.New("user role not found")
ErrTeamNotFound = errors.New("team not found")
ErrPermissionNotFound = errors.New("permission not found")
ErrRoleFailedGenerateUniqueUID = errors.New("failed to generate role definition UID")
ErrVersionLE = errors.New("the provided role version is smaller than or equal to stored role")
)
// Role is the model for Role in RBAC.
type Role struct {
ID int64 `json:"id" xorm:"pk autoincr 'id'"`
OrgID int64 `json:"orgId" xorm:"org_id"`
Version int64 `json:"version"`
UID string `xorm:"uid" json:"uid"`
Name string `json:"name"`
Description string `json:"description"`
Updated time.Time `json:"updated"`
Created time.Time `json:"created"`
}
type RoleDTO struct {
ID int64 `json:"id" xorm:"pk autoincr 'id'"`
OrgID int64 `json:"orgId" xorm:"org_id"`
Version int64 `json:"version"`
UID string `xorm:"uid" json:"uid"`
Name string `json:"name"`
Description string `json:"description"`
Permissions []Permission `json:"permissions,omitempty"`
Updated time.Time `json:"updated"`
Created time.Time `json:"created"`
}
// Permission is the model for Permission in RBAC.
type Permission struct {
ID int64 `json:"id" xorm:"pk autoincr 'id'"`
RoleID int64 `json:"-" xorm:"role_id"`
Permission string `json:"permission"`
Scope string `json:"scope"`
Updated time.Time `json:"updated"`
Created time.Time `json:"created"`
}
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"`
Role string
Updated time.Time
Created time.Time
}
type GetTeamRolesQuery struct {
OrgID int64 `json:"-"`
TeamID int64 `json:"teamId"`
}
type GetUserRolesQuery struct {
OrgID int64 `json:"-"`
UserID int64 `json:"userId"`
Roles []string
}
type GetUserPermissionsQuery struct {
OrgID int64 `json:"-"`
UserID int64 `json:"userId"`
Roles []string
}
type CreatePermissionCommand struct {
RoleID int64 `json:"roleId"`
Permission string
Scope string
}
type UpdatePermissionCommand struct {
ID int64 `json:"id"`
Permission string
Scope string
}
type DeletePermissionCommand struct {
ID int64 `json:"id"`
}
type CreateRoleCommand struct {
OrgID int64 `json:"-"`
UID string `json:"uid"`
Version int64 `json:"version"`
Name string `json:"name"`
Description string `json:"description"`
}
type CreateRoleWithPermissionsCommand struct {
OrgID int64 `json:"orgId"`
UID string `json:"uid"`
Version int64 `json:"version"`
Name string `json:"name"`
Description string `json:"description"`
Permissions []Permission `json:"permissions"`
}
type UpdateRoleCommand struct {
ID int64 `json:"id"`
OrgID int64 `json:"orgId"`
Version int64 `json:"version"`
UID string `json:"uid"`
Name string `json:"name"`
Description string `json:"description"`
Permissions []Permission `json:"permissions"`
}
type DeleteRoleCommand struct {
ID int64 `json:"id"`
UID string `json:"uid"`
OrgID int64 `json:"org_id"`
}
type AddTeamRoleCommand struct {
OrgID int64 `json:"org_id"`
RoleID int64 `json:"role_id"`
TeamID int64 `json:"team_id"`
}
type RemoveTeamRoleCommand struct {
OrgID int64 `json:"org_id"`
RoleID int64 `json:"role_id"`
TeamID int64 `json:"team_id"`
}
type AddUserRoleCommand struct {
OrgID int64 `json:"org_id"`
RoleID int64 `json:"role_id"`
UserID int64 `json:"user_id"`
}
type RemoveUserRoleCommand struct {
OrgID int64 `json:"org_id"`
RoleID int64 `json:"role_id"`
UserID int64 `json:"user_id"`
}
type EvaluationResult struct {
HasAccess bool
Meta interface{}
}
func (p RoleDTO) Role() Role {
return Role{
ID: p.ID,
OrgID: p.OrgID,
Name: p.Name,
Description: p.Description,
Updated: p.Updated,
Created: p.Created,
}
}

@ -0,0 +1,219 @@
package seeder
import (
"context"
"errors"
"fmt"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/accesscontrol"
)
type seeder struct {
Store accesscontrol.Store
log log.Logger
}
var builtInRoles = []accesscontrol.RoleDTO{
{
Name: "grafana:builtin:users:read:self",
Version: 1,
Permissions: []accesscontrol.Permission{
{
Permission: "users:read",
Scope: "users:self",
},
{
Permission: "users.tokens:list",
Scope: "users:self",
},
{
Permission: "users.teams:read",
Scope: "users:self",
},
},
},
}
// FIXME: Make sure builtin grants can be removed without being recreated
var builtInRoleGrants = map[string][]string{
"grafana:builtin:users:read:self": {
"Viewer",
},
}
func NewSeeder(s accesscontrol.AccessControl, log log.Logger) *seeder {
return &seeder{Store: s, log: log}
}
func (s *seeder) Seed(ctx context.Context, orgID int64) error {
err := s.seed(ctx, orgID, builtInRoles, builtInRoleGrants)
return err
}
func (s *seeder) seed(ctx context.Context, orgID int64, roles []accesscontrol.RoleDTO, roleGrants map[string][]string) error {
// FIXME: As this will run on startup, we want to optimize running this
existingRoles, err := s.Store.GetRoles(ctx, orgID)
if err != nil {
return err
}
roleSet := map[string]*accesscontrol.Role{}
for _, role := range existingRoles {
if role == nil {
continue
}
roleSet[role.Name] = role
}
for _, role := range roles {
role.OrgID = orgID
current, exists := roleSet[role.Name]
if exists {
if role.Version <= current.Version {
continue
}
}
roleID, err := s.createOrUpdateRole(ctx, role, current)
if err != nil {
s.log.Error("failed to create/update role", "name", role.Name, "err", err)
continue
}
if builtinRoles, exists := roleGrants[role.Name]; exists {
for _, builtinRole := range builtinRoles {
err := s.Store.AddBuiltinRole(ctx, orgID, roleID, builtinRole)
if err != nil && !errors.Is(err, accesscontrol.ErrUserRoleAlreadyAdded) {
s.log.Error("failed to assign role to role",
"name", role.Name,
"role", builtinRole,
"err", err,
)
return err
}
}
}
}
return nil
}
func (s *seeder) createOrUpdateRole(ctx context.Context, role accesscontrol.RoleDTO, old *accesscontrol.Role) (int64, error) {
if role.Version == 0 {
return 0, fmt.Errorf("error when seeding '%s': all seeder roles must have a version", role.Name)
}
if old == nil {
p, err := s.Store.CreateRoleWithPermissions(ctx, accesscontrol.CreateRoleWithPermissionsCommand{
OrgID: role.OrgID,
Version: role.Version,
Name: role.Name,
Description: role.Description,
Permissions: role.Permissions,
})
if err != nil {
return 0, err
}
return p.ID, nil
}
_, err := s.Store.UpdateRole(ctx, accesscontrol.UpdateRoleCommand{
UID: old.UID,
Name: role.Name,
Description: role.Description,
Version: role.Version,
})
if err != nil {
if errors.Is(err, accesscontrol.ErrVersionLE) {
return old.ID, nil
}
return 0, err
}
existingPermissions, err := s.Store.GetRolePermissions(ctx, old.ID)
if err != nil {
return 0, fmt.Errorf("failed to get current permissions for role '%s': %w", role.Name, err)
}
err = s.idempotentUpdatePermissions(ctx, old.ID, role.Permissions, existingPermissions)
if err != nil {
return 0, fmt.Errorf("failed to update role permissions for role '%s': %w", role.Name, err)
}
return old.ID, nil
}
func (s *seeder) idempotentUpdatePermissions(ctx context.Context, roleID int64, new []accesscontrol.Permission, old []accesscontrol.Permission) error {
if roleID == 0 {
return fmt.Errorf("refusing to add permissions to role with ID 0 (it should not exist)")
}
added, removed := diffPermissionList(new, old)
for _, p := range added {
_, err := s.Store.CreatePermission(ctx, accesscontrol.CreatePermissionCommand{
RoleID: roleID,
Permission: p.Permission,
Scope: p.Scope,
})
if err != nil {
return fmt.Errorf("could not create permission %s (%s): %w", p.Permission, p.Scope, err)
}
}
for _, p := range removed {
err := s.Store.DeletePermission(ctx, &accesscontrol.DeletePermissionCommand{
ID: p.ID,
})
if err != nil {
return fmt.Errorf("could not delete permission %s (%s): %w", p.Permission, p.Scope, err)
}
}
return nil
}
func diffPermissionList(new, old []accesscontrol.Permission) (added, removed []accesscontrol.Permission) {
newMap, oldMap := permissionMap(new), permissionMap(old)
added = []accesscontrol.Permission{}
removed = []accesscontrol.Permission{}
for _, p := range newMap {
if _, exists := oldMap[permissionTuple{
Permission: p.Permission,
Scope: p.Scope,
}]; exists {
continue
}
added = append(added, p)
}
for _, p := range oldMap {
if _, exists := newMap[permissionTuple{
Permission: p.Permission,
Scope: p.Scope,
}]; exists {
continue
}
removed = append(removed, p)
}
return added, removed
}
type permissionTuple struct {
Permission string
Scope string
}
func permissionMap(l []accesscontrol.Permission) map[permissionTuple]accesscontrol.Permission {
m := make(map[permissionTuple]accesscontrol.Permission, len(l))
for _, p := range l {
m[permissionTuple{
Permission: p.Permission,
Scope: p.Scope,
}] = p
}
return m
}

@ -0,0 +1,156 @@
package seeder
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/setting"
)
// accessControlStoreTestImpl is a test store implementation which additionally executes a database migrations
type accessControlStoreTestImpl struct {
database.AccessControlStore
}
func (ac *accessControlStoreTestImpl) AddMigration(mg *migrator.Migrator) {
database.AddAccessControlMigrations(mg)
}
func setupTestEnv(t testing.TB) *accessControlStoreTestImpl {
t.Helper()
cfg := setting.NewCfg()
store := overrideDatabaseInRegistry(t, cfg)
sqlStore := sqlstore.InitTestDB(t)
store.SQLStore = sqlStore
err := store.Init()
require.NoError(t, err)
return &store
}
func overrideDatabaseInRegistry(t testing.TB, cfg *setting.Cfg) accessControlStoreTestImpl {
t.Helper()
store := accessControlStoreTestImpl{
AccessControlStore: database.AccessControlStore{
SQLStore: nil,
},
}
overrideServiceFunc := func(descriptor registry.Descriptor) (*registry.Descriptor, bool) {
if _, ok := descriptor.Instance.(*database.AccessControlStore); ok {
return &registry.Descriptor{
Name: "Database",
Instance: &store,
InitPriority: descriptor.InitPriority,
}, true
}
return nil, false
}
registry.RegisterOverride(overrideServiceFunc)
return store
}
func TestSeeder(t *testing.T) {
ac := setupTestEnv(t)
s := &seeder{
Store: ac,
log: log.New("accesscontrol-test"),
}
v1 := accesscontrol.RoleDTO{
OrgID: 1,
Name: "grafana:tests:fake",
Version: 1,
Permissions: []accesscontrol.Permission{
{
Permission: "ice_cream:eat",
Scope: "flavor:vanilla",
},
{
Permission: "ice_cream:eat",
Scope: "flavor:chocolate",
},
},
}
v2 := accesscontrol.RoleDTO{
OrgID: 1,
Name: "grafana:tests:fake",
Version: 2,
Permissions: []accesscontrol.Permission{
{
Permission: "ice_cream:eat",
Scope: "flavor:vanilla",
},
{
Permission: "ice_cream:serve",
Scope: "flavor:mint",
},
{
Permission: "candy.liquorice:eat",
Scope: "",
},
},
}
t.Run("create role", func(t *testing.T) {
id, err := s.createOrUpdateRole(
context.Background(),
v1,
nil,
)
require.NoError(t, err)
assert.NotZero(t, id)
p, err := s.Store.GetRole(context.Background(), 1, id)
require.NoError(t, err)
lookup := permissionMap(p.Permissions)
assert.Contains(t, lookup, permissionTuple{
Permission: "ice_cream:eat",
Scope: "flavor:vanilla",
})
assert.Contains(t, lookup, permissionTuple{
Permission: "ice_cream:eat",
Scope: "flavor:chocolate",
})
role := p.Role()
t.Run("update to same version", func(t *testing.T) {
err := s.seed(context.Background(), 1, []accesscontrol.RoleDTO{v1}, nil)
require.NoError(t, err)
})
t.Run("update to new role version", func(t *testing.T) {
err := s.seed(context.Background(), 1, []accesscontrol.RoleDTO{v2}, nil)
require.NoError(t, err)
p, err := s.Store.GetRole(context.Background(), 1, role.ID)
require.NoError(t, err)
assert.Len(t, p.Permissions, len(v2.Permissions))
lookup := permissionMap(p.Permissions)
assert.Contains(t, lookup, permissionTuple{
Permission: "candy.liquorice:eat",
Scope: "",
})
assert.NotContains(t, lookup, permissionTuple{
Permission: "ice_cream:eat",
Scope: "flavor:chocolate",
})
})
})
}

@ -0,0 +1,121 @@
package testing
import (
"context"
"testing"
"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 RoleTestCase struct {
Name string
UID string
Permissions []PermissionTestCase
}
type PermissionTestCase struct {
Permission string
Scope string
}
func CreateRole(t *testing.T, ac accesscontrol.Store, p RoleTestCase) *accesscontrol.RoleDTO {
createRoleCmd := accesscontrol.CreateRoleWithPermissionsCommand{
OrgID: 1,
UID: p.UID,
Name: p.Name,
Permissions: []accesscontrol.Permission{},
}
for _, perm := range p.Permissions {
createRoleCmd.Permissions = append(createRoleCmd.Permissions, accesscontrol.Permission{
Permission: perm.Permission,
Scope: perm.Scope,
})
}
res, err := ac.CreateRoleWithPermissions(context.Background(), createRoleCmd)
require.NoError(t, err)
return res
}
func CreateUserWithRole(t *testing.T, db *sqlstore.SQLStore, ac accesscontrol.Store, user string, roles []RoleTestCase) {
createUserCmd := models.CreateUserCommand{
Email: user + "@test.com",
Name: user,
Login: user,
OrgId: 1,
}
u, err := db.CreateUser(context.Background(), createUserCmd)
require.NoError(t, err)
userId := u.Id
for _, p := range roles {
createRoleCmd := accesscontrol.CreateRoleCommand{
OrgID: 1,
Name: p.Name,
}
res, err := ac.CreateRole(context.Background(), createRoleCmd)
require.NoError(t, err)
roleId := res.ID
for _, perm := range p.Permissions {
permCmd := accesscontrol.CreatePermissionCommand{
RoleID: roleId,
Permission: perm.Permission,
Scope: perm.Scope,
}
_, err := ac.CreatePermission(context.Background(), permCmd)
require.NoError(t, err)
}
addUserRoleCmd := accesscontrol.AddUserRoleCommand{
OrgID: 1,
RoleID: roleId,
UserID: userId,
}
err = ac.AddUserRole(&addUserRoleCmd)
require.NoError(t, err)
}
}
func CreateTeamWithRole(t *testing.T, db *sqlstore.SQLStore, ac accesscontrol.Store, teamname string, roles []RoleTestCase) {
email, orgID := teamname+"@test.com", int64(1)
team, err := db.CreateTeam(teamname, email, orgID)
require.NoError(t, err)
teamId := team.Id
for _, p := range roles {
createRoleCmd := accesscontrol.CreateRoleCommand{
OrgID: orgID,
Name: p.Name,
}
res, err := ac.CreateRole(context.Background(), createRoleCmd)
require.NoError(t, err)
roleId := res.ID
for _, perm := range p.Permissions {
permCmd := accesscontrol.CreatePermissionCommand{
RoleID: roleId,
Permission: perm.Permission,
Scope: perm.Scope,
}
_, err := ac.CreatePermission(context.Background(), permCmd)
require.NoError(t, err)
}
addTeamRoleCmd := accesscontrol.AddTeamRoleCommand{
OrgID: 1,
RoleID: roleId,
TeamID: teamId,
}
err = ac.AddTeamRole(&addTeamRoleCmd)
require.NoError(t, err)
}
}

@ -0,0 +1,109 @@
package testing
import (
"context"
"fmt"
"math"
"testing"
"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 (
usernamePrefix = "user"
teamPrefix = "team"
PermissionsPerRole = 10
UsersPerTeam = 10
)
func GenerateRoles(b *testing.B, db *sqlstore.SQLStore, ac accesscontrol.Store, rolesPerUser, users int) {
numberOfTeams := int(math.Ceil(float64(users) / UsersPerTeam))
globalUserId := 0
for i := 0; i < numberOfTeams; i++ {
// Create team
teamName := fmt.Sprintf("%s%v", teamPrefix, i)
teamEmail := fmt.Sprintf("%s@test.com", teamName)
team, err := db.CreateTeam(teamName, teamEmail, 1)
require.NoError(b, err)
teamId := team.Id
// Create team roles
for j := 0; j < rolesPerUser; j++ {
roleName := fmt.Sprintf("role_%s_%v", teamName, j)
createRoleCmd := accesscontrol.CreateRoleCommand{OrgID: 1, Name: roleName}
res, err := ac.CreateRole(context.Background(), createRoleCmd)
require.NoError(b, err)
roleId := res.ID
for k := 0; k < PermissionsPerRole; k++ {
permission := fmt.Sprintf("permission_%v", k)
scope := fmt.Sprintf("scope_%v", k)
permCmd := accesscontrol.CreatePermissionCommand{
RoleID: roleId,
Permission: permission,
Scope: scope,
}
_, err := ac.CreatePermission(context.Background(), permCmd)
require.NoError(b, err)
}
addTeamRoleCmd := accesscontrol.AddTeamRoleCommand{
OrgID: 1,
RoleID: roleId,
TeamID: teamId,
}
err = ac.AddTeamRole(&addTeamRoleCmd)
require.NoError(b, err)
}
// Create team users
for u := 0; u < UsersPerTeam; u++ {
userName := fmt.Sprintf("%s%v", usernamePrefix, globalUserId)
userEmail := fmt.Sprintf("%s@test.com", 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++
// Create user roles
for j := 0; j < rolesPerUser; j++ {
roleName := fmt.Sprintf("role_%s_%v", userName, j)
createRoleCmd := accesscontrol.CreateRoleCommand{OrgID: 1, Name: roleName}
res, err := ac.CreateRole(context.Background(), createRoleCmd)
require.NoError(b, err)
roleId := res.ID
for k := 0; k < PermissionsPerRole; k++ {
permission := fmt.Sprintf("permission_%v", k)
scope := fmt.Sprintf("scope_%v", k)
permCmd := accesscontrol.CreatePermissionCommand{
RoleID: roleId,
Permission: permission,
Scope: scope,
}
_, err := ac.CreatePermission(context.Background(), permCmd)
require.NoError(b, err)
}
addUserRoleCmd := accesscontrol.AddUserRoleCommand{
OrgID: 1,
RoleID: roleId,
UserID: userId,
}
err = ac.AddUserRole(&addUserRoleCmd)
require.NoError(b, err)
}
err = db.AddTeamMember(userId, 1, teamId, false, 1)
require.NoError(b, err)
}
}
}

@ -0,0 +1,84 @@
#!/usr/bin/env bash
COMMIT_HASH=$(git log -n 1 | grep -Po "(?<=commit )[0-9a-z]{40}")
COMMIT_HASH=${COMMIT_HASH::7}
BENCH_FILE="tmp/bench_${COMMIT_HASH}.txt"
BENCH_GRAPH="tmp/bench_${COMMIT_HASH}.html"
# Run benchmark
go test -benchmem -run=^$ -bench . github.com/grafana/grafana/pkg/services/accesscontrol/database | tee "${BENCH_FILE}"
CHART_DATA_USERS=$(grep -oP "^BenchmarkRolesUsers([^[:blank:]]+)[[:blank:]]+[0-9]+[[:blank:]]+[0-9]+" "${BENCH_FILE}" |
sed -E 's/Benchmark([^[:blank:]]+)[[:blank:]]+[0-9]+[[:blank:]]+([0-9]+)/\1 \2/' |
sed -E 's/^[[:alpha:]]+([0-9]+)_([0-9]+)-[0-9]+[[:blank:]]+(.*)/\2 \3/' |
sed -E 's/([^[:blank:]]+)[[:blank:]]+([^[:blank:]]+)/\[\1, \2],\n/'
)
CHART_DATA_ROLES=$(grep -oP "^BenchmarkRolesPerUser([^[:blank:]]+)[[:blank:]]+[0-9]+[[:blank:]]+[0-9]+" "${BENCH_FILE}" |
sed -E 's/Benchmark([^[:blank:]]+)[[:blank:]]+[0-9]+[[:blank:]]+([0-9]+)/\1 \2/' |
sed -E 's/^[[:alpha:]]+([0-9]+)_([0-9]+)-[0-9]+[[:blank:]]+(.*)/\1 \3/' |
sed -E 's/([^[:blank:]]+)[[:blank:]]+([^[:blank:]]+)/\[\1, \2],\n/'
)
HTML_CHART="<html>
<head>
<script type='text/javascript' src='https://www.gstatic.com/charts/loader.js'></script>
<script type='text/javascript'>
google.charts.load('current', {'packages':['corechart']});
google.charts.setOnLoadCallback(drawChart);
function drawChart() {
var dataUsers = google.visualization.arrayToDataTable([
['case', 'time'],
${CHART_DATA_USERS}
]);
var dataPolicies = google.visualization.arrayToDataTable([
['case', 'time'],
${CHART_DATA_ROLES}
]);
var options = {
title: 'Roles Performance (commit ${COMMIT_HASH})',
legend: 'none',
vAxis: {
title: 'Execution time (ns)',
minValue: 0,
},
hAxis: {
title: 'Number of users',
minValue: 0,
},
trendlines: {
0: {
type: 'polynomial',
degree: 3,
color: 'purple',
lineWidth: 10,
opacity: 0.2,
},
}
};
var chartUsers = new google.visualization.ScatterChart(document.getElementById('chart_users'));
var chartPolicies = new google.visualization.ScatterChart(document.getElementById('chart_policies'));
chartUsers.draw(dataUsers, options);
var chartPoliciesOptions = options;
chartPoliciesOptions.hAxis.title = 'Number of policies per user';
chartPoliciesOptions.trendlines[0].color = 'red';
chartPolicies.draw(dataPolicies, options);
}
</script>
</head>
<body>
<div style='display: flex'>
<div id='chart_users' style='width: 900px; height: 500px'></div>
<div id='chart_policies' style='width: 900px; height: 500px'></div>
</div>
</body>
</html>
"
echo "${HTML_CHART}" | tee "${BENCH_GRAPH}"
Loading…
Cancel
Save