mirror of https://github.com/grafana/grafana
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
parent
fd9dee87e4
commit
20bd591bea
@ -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 ®istry.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 ®istry.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 ®istry.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…
Reference in new issue