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:
co-authored by
Alexander Zobnin
Emil Tullstedt
Arve Knudsen
Marcus Efraimsson
parent
fd9dee87e4
commit
20bd591bea
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user