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>
This commit is contained in:
Alexander Zobnin
2021-03-22 13:22:48 +01:00
committed by GitHub
co-authored by Alexander Zobnin Emil Tullstedt Arve Knudsen Marcus Efraimsson
parent fd9dee87e4
commit 20bd591bea
18 changed files with 2556 additions and 10 deletions
@@ -0,0 +1,74 @@
package manager
import (
"context"
"github.com/gobwas/glob"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
)
const roleGrafanaAdmin = "Grafana Admin"
func (m *Manager) Evaluate(ctx context.Context, user *models.SignedInUser, permission string, scope ...string) (bool, error) {
roles := []string{string(user.OrgRole)}
for _, role := range user.OrgRole.Children() {
roles = append(roles, string(role))
}
if user.IsGrafanaAdmin {
roles = append(roles, roleGrafanaAdmin)
}
res, err := m.GetUserPermissions(ctx, accesscontrol.GetUserPermissionsQuery{
OrgID: user.OrgId,
UserID: user.UserId,
Roles: roles,
})
if err != nil {
return false, err
}
ok, dbScopes := extractPermission(res, permission)
if !ok {
return false, nil
}
for _, s := range scope {
var match bool
for dbScope := range dbScopes {
rule, err := glob.Compile(dbScope, ':', '/')
if err != nil {
return false, err
}
match = rule.Match(s)
if match {
break
}
}
if !match {
return false, nil
}
}
return true, nil
}
func extractPermission(permissions []*accesscontrol.Permission, permission string) (bool, map[string]struct{}) {
scopes := map[string]struct{}{}
ok := false
for _, p := range permissions {
if p == nil {
continue
}
if p.Permission == permission {
ok = true
scopes[p.Scope] = struct{}{}
}
}
return ok, scopes
}
@@ -0,0 +1,53 @@
package manager
import (
"context"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
"github.com/grafana/grafana/pkg/services/accesscontrol/seeder"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/setting"
)
// Manager is the service implementing role based access control.
type Manager struct {
Cfg *setting.Cfg `inject:""`
RouteRegister routing.RouteRegister `inject:""`
Log log.Logger
*database.AccessControlStore
}
func init() {
registry.RegisterService(&Manager{})
}
// Init initializes the Manager.
func (m *Manager) Init() error {
m.Log = log.New("accesscontrol")
seeder := seeder.NewSeeder(m, m.Log)
// TODO: Seed all orgs
err := seeder.Seed(context.TODO(), 1)
if err != nil {
return err
}
return nil
}
func (m *Manager) IsDisabled() bool {
_, exists := m.Cfg.FeatureToggles["accesscontrol"]
return !exists
}
func (m *Manager) AddMigration(mg *migrator.Migrator) {
if m.IsDisabled() {
return
}
database.AddAccessControlMigrations(mg)
}
@@ -0,0 +1,118 @@
package manager
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
actesting "github.com/grafana/grafana/pkg/services/accesscontrol/testing"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
)
func setupTestEnv(t testing.TB) *Manager {
t.Helper()
cfg := setting.NewCfg()
cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
ac := overrideAccessControlInRegistry(t, cfg)
sqlStore := sqlstore.InitTestDB(t)
ac.AccessControlStore.SQLStore = sqlStore
err := ac.Init()
require.NoError(t, err)
return &ac
}
func overrideAccessControlInRegistry(t testing.TB, cfg *setting.Cfg) Manager {
t.Helper()
ac := Manager{
Cfg: cfg,
RouteRegister: routing.NewRouteRegister(),
Log: log.New("accesscontrol-test"),
AccessControlStore: &database.AccessControlStore{
SQLStore: nil,
},
}
overrideServiceFunc := func(descriptor registry.Descriptor) (*registry.Descriptor, bool) {
if _, ok := descriptor.Instance.(*Manager); ok {
return &registry.Descriptor{
Name: "AccessControl",
Instance: &ac,
InitPriority: descriptor.InitPriority,
}, true
}
return nil, false
}
registry.RegisterOverride(overrideServiceFunc)
return ac
}
type evaluatingPermissionsTestCase struct {
desc string
userName string
roles []actesting.RoleTestCase
}
func TestEvaluatingPermissions(t *testing.T) {
testCases := []evaluatingPermissionsTestCase{
{
desc: "should successfully evaluate access to the endpoint",
userName: "testuser",
roles: []actesting.RoleTestCase{
{
Name: "CreateUser", Permissions: []actesting.PermissionTestCase{
{Scope: "/api/admin/users", Permission: "post"},
{Scope: "/api/report", Permission: "get"},
},
},
},
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
ac := setupTestEnv(t)
t.Cleanup(registry.ClearOverrides)
actesting.CreateUserWithRole(t, ac.SQLStore, ac, tc.userName, tc.roles)
userQuery := models.GetUserByLoginQuery{
LoginOrEmail: tc.userName,
}
err := sqlstore.GetUserByLogin(&userQuery)
require.NoError(t, err)
userRolesQuery := accesscontrol.GetUserRolesQuery{
OrgID: 1,
UserID: userQuery.Result.Id,
}
res, err := ac.GetUserRoles(context.Background(), userRolesQuery)
require.NoError(t, err)
assert.Len(t, res, len(tc.roles))
userPermissionsQuery := accesscontrol.GetUserPermissionsQuery{
OrgID: 1,
UserID: userQuery.Result.Id,
}
permissions, err := ac.GetUserPermissions(context.Background(), userPermissionsQuery)
require.NoError(t, err)
assert.Len(t, permissions, len(tc.roles[0].Permissions))
})
}
}