Access Control: refactor permission evaluator to be more flexible (#35996)

* add a more flexible way to create permissions

* update interface for accesscontrol to use new eval interface

* use new eval interface

* update middleware to use new eval interface

* remove evaluator function and move metrics to service

* add tests for accesscontrol middleware

* Remove failed function from interface and update inejct to create a new
evaluator

* Change name

* Support Several sopes for a permission


* use evaluator and update fakeAccessControl

* Implement String that will return string representation of permissions
for an evaluator

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>

Co-authored-by: Emil Tullstedt <emil.tullstedt@grafana.com>
This commit is contained in:
Karl Persson
2021-08-24 11:36:28 +02:00
committed by GitHub
co-authored by Gabriel MABILLE Emil Tullstedt
parent 9d8f61c738
commit 7ebf4027a7
15 changed files with 869 additions and 349 deletions
+19 -6
View File
@@ -8,13 +8,13 @@ import (
)
type AccessControl interface {
// Evaluate evaluates access to the given resource.
Evaluate(ctx context.Context, user *models.SignedInUser, permission string, scope ...string) (bool, error)
// Evaluate evaluates access to the given resources.
Evaluate(ctx context.Context, user *models.SignedInUser, evaluator Evaluator) (bool, error)
// GetUserPermissions returns user permissions.
GetUserPermissions(ctx context.Context, user *models.SignedInUser) ([]*Permission, error)
// Middleware checks if service disabled or not to switch to fallback authorization.
//IsDisabled returns if access control is enabled or not
IsDisabled() bool
// DeclareFixedRoles allow the caller to declare, to the service, fixed roles and their
@@ -22,13 +22,13 @@ type AccessControl interface {
DeclareFixedRoles(...RoleRegistration) error
}
func HasAccess(ac AccessControl, c *models.ReqContext) func(fallback func(*models.ReqContext) bool, permission string, scopes ...string) bool {
return func(fallback func(*models.ReqContext) bool, permission string, scopes ...string) bool {
func HasAccess(ac AccessControl, c *models.ReqContext) func(fallback func(*models.ReqContext) bool, evaluator Evaluator) bool {
return func(fallback func(*models.ReqContext) bool, evaluator Evaluator) bool {
if ac.IsDisabled() {
return fallback(c)
}
hasAccess, err := ac.Evaluate(c.Req.Context(), c.SignedInUser, permission, scopes...)
hasAccess, err := ac.Evaluate(c.Req.Context(), c.SignedInUser, evaluator)
if err != nil {
c.Logger.Error("Error from access control system", "error", err)
return false
@@ -55,6 +55,19 @@ func BuildPermissionsMap(permissions []*Permission) map[string]bool {
return permissionsMap
}
// GroupScopesByAction will group scopes on action
func GroupScopesByAction(permissions []*Permission) map[string]map[string]struct{} {
m := make(map[string]map[string]struct{})
for _, p := range permissions {
if _, ok := m[p.Action]; ok {
m[p.Action][p.Scope] = struct{}{}
} else {
m[p.Action] = map[string]struct{}{p.Scope: {}}
}
}
return m
}
func ValidateScope(scope string) bool {
prefix, last := scope[:len(scope)-1], scope[len(scope)-1]
// verify that last char is either ':' or '/' if last character of scope is '*'
+194
View File
@@ -0,0 +1,194 @@
package accesscontrol
import (
"bytes"
"fmt"
"html/template"
"strings"
"github.com/grafana/grafana/pkg/infra/log"
)
var logger = log.New("accesscontrol.evaluator")
type Evaluator interface {
// Evaluate permissions that are grouped by action
Evaluate(permissions map[string]map[string]struct{}) (bool, error)
// Inject params into the evaluator's templated scopes. e.g. "settings:" + eval.Parameters(":id") and returns a new Evaluator
Inject(params map[string]string) (Evaluator, error)
// String returns a string representation of permission required by the evaluator
String() string
}
var _ Evaluator = new(permissionEvaluator)
// EvalPermission returns an evaluator that will require all scopes in combination with action to match
func EvalPermission(action string, scopes ...string) Evaluator {
return permissionEvaluator{Action: action, Scopes: scopes}
}
type permissionEvaluator struct {
Action string
Scopes []string
}
func (p permissionEvaluator) Evaluate(permissions map[string]map[string]struct{}) (bool, error) {
userScopes, ok := permissions[p.Action]
if !ok {
return false, nil
}
if len(p.Scopes) == 0 {
return true, nil
}
for _, target := range p.Scopes {
var err error
var matches bool
for scope := range userScopes {
matches, err = match(scope, target)
if err != nil {
return false, err
}
if matches {
break
}
}
if !matches {
return false, nil
}
}
return true, nil
}
func match(scope, target string) (bool, error) {
if scope == "" {
return false, nil
}
if !ValidateScope(scope) {
logger.Error(
"invalid scope",
"scope", scope,
"reason", "scopes should not contain meta-characters like * or ?, except in the last position",
)
return false, nil
}
prefix, last := scope[:len(scope)-1], scope[len(scope)-1]
//Prefix match
if last == '*' {
if strings.HasPrefix(target, prefix) {
logger.Debug("matched scope", "user scope", scope, "target scope", target)
return true, nil
}
}
return scope == target, nil
}
func (p permissionEvaluator) Inject(params map[string]string) (Evaluator, error) {
scopes := make([]string, 0, len(p.Scopes))
for _, scope := range p.Scopes {
tmpl, err := template.New("scope").Parse(scope)
if err != nil {
return nil, err
}
var buf bytes.Buffer
if err = tmpl.Execute(&buf, params); err != nil {
return nil, err
}
scopes = append(scopes, buf.String())
}
return EvalPermission(p.Action, scopes...), nil
}
func (p permissionEvaluator) String() string {
return fmt.Sprintf("action:%s scopes:%s", p.Action, strings.Join(p.Scopes, ", "))
}
var _ Evaluator = new(allEvaluator)
// EvalAll returns evaluator that requires all passed evaluators to evaluate to true
func EvalAll(allOf ...Evaluator) Evaluator {
return allEvaluator{allOf: allOf}
}
type allEvaluator struct {
allOf []Evaluator
}
func (a allEvaluator) Evaluate(permissions map[string]map[string]struct{}) (bool, error) {
for _, e := range a.allOf {
if ok, err := e.Evaluate(permissions); !ok || err != nil {
return false, err
}
}
return true, nil
}
func (a allEvaluator) Inject(params map[string]string) (Evaluator, error) {
var injected []Evaluator
for _, e := range a.allOf {
i, err := e.Inject(params)
if err != nil {
return nil, err
}
injected = append(injected, i)
}
return EvalAll(injected...), nil
}
func (a allEvaluator) String() string {
permissions := make([]string, 0, len(a.allOf))
for _, e := range a.allOf {
permissions = append(permissions, e.String())
}
return fmt.Sprintf("all(%s)", strings.Join(permissions, " "))
}
var _ Evaluator = new(anyEvaluator)
// EvalAny returns evaluator that requires at least one of passed evaluators to evaluate to true
func EvalAny(anyOf ...Evaluator) Evaluator {
return anyEvaluator{anyOf: anyOf}
}
type anyEvaluator struct {
anyOf []Evaluator
}
func (a anyEvaluator) Evaluate(permissions map[string]map[string]struct{}) (bool, error) {
for _, e := range a.anyOf {
ok, err := e.Evaluate(permissions)
if err != nil {
return false, err
}
if ok {
return true, nil
}
}
return false, nil
}
func (a anyEvaluator) Inject(params map[string]string) (Evaluator, error) {
var injected []Evaluator
for _, e := range a.anyOf {
i, err := e.Inject(params)
if err != nil {
return nil, err
}
injected = append(injected, i)
}
return EvalAny(injected...), nil
}
func (a anyEvaluator) String() string {
permissions := make([]string, 0, len(a.anyOf))
for _, e := range a.anyOf {
permissions = append(permissions, e.String())
}
return fmt.Sprintf("any(%s)", strings.Join(permissions, " "))
}
@@ -1,101 +0,0 @@
package evaluator
import (
"context"
"fmt"
"strings"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/prometheus/client_golang/prometheus"
)
var logger = log.New("accesscontrol.evaluator")
// Evaluate evaluates access to the given resource, using provided AccessControl instance.
// Scopes are evaluated with an `OR` relationship.
func Evaluate(ctx context.Context, ac accesscontrol.AccessControl, user *models.SignedInUser, action string, scope ...string) (bool, error) {
timer := prometheus.NewTimer(metrics.MAccessEvaluationsSummary)
defer timer.ObserveDuration()
metrics.MAccessEvaluationCount.Inc()
userPermissions, err := ac.GetUserPermissions(ctx, user)
if err != nil {
return false, err
}
ok, dbScopes := extractScopes(userPermissions, action)
if !ok {
return false, nil
}
res, err := evaluateScope(dbScopes, scope...)
return res, err
}
func evaluateScope(dbScopes map[string]struct{}, targetScopes ...string) (bool, error) {
if len(targetScopes) == 0 {
return true, nil
}
for _, s := range targetScopes {
for dbScope := range dbScopes {
if dbScope == "" {
continue
}
if !accesscontrol.ValidateScope(dbScope) {
logger.Error(
"invalid scope",
"reason", fmt.Sprintf("%v should not contain meta-characters like * or ?, except in the last position", dbScope),
"scope", dbScope,
)
continue
}
prefix, last := dbScope[:len(dbScope)-1], dbScope[len(dbScope)-1]
//Prefix match
if last == '*' {
if strings.HasPrefix(s, prefix) {
logger.Debug(
"matched scope",
"reason", fmt.Sprintf("matched request scope %v against resource scope %v", dbScope, s),
"request scope", dbScope,
"resource scope", s,
)
return true, nil
}
}
if s == dbScope {
return true, nil
}
}
}
logger.Debug(
"access control failed",
"request scope", dbScopes,
"resource scope", targetScopes,
"reason", fmt.Sprintf("Could not match resource scopes %v with request scopes %v", dbScopes, targetScopes),
)
return false, nil
}
func extractScopes(permissions []*accesscontrol.Permission, targetAction string) (bool, map[string]struct{}) {
scopes := map[string]struct{}{}
ok := false
for _, p := range permissions {
if p == nil {
continue
}
if p.Action == targetAction {
ok = true
scopes[p.Scope] = struct{}{}
}
}
return ok, scopes
}
@@ -1,112 +0,0 @@
package evaluator
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/accesscontrol"
)
func TestExtractPermission(t *testing.T) {
const targetPermission = "permissions:create"
userPermissions := []*accesscontrol.Permission{
{
Action: "permissions:create",
Scope: "teams:*",
},
{
Action: "permissions:create",
Scope: "permissions:*",
},
{
Action: "permissions:remove",
Scope: "permissions:*",
},
}
expectedScopes := map[string]struct{}{
"permissions:*": {},
"teams:*": {},
}
ok, scopes := extractScopes(userPermissions, targetPermission)
assert.True(t, ok)
assert.Equal(t, expectedScopes, scopes)
}
func TestEvaluatePermissions(t *testing.T) {
tests := []struct {
Name string
HasScopes map[string]struct{}
NeedAnyScope []string
Valid bool
}{
{
Name: "Base",
HasScopes: map[string]struct{}{},
NeedAnyScope: []string{},
Valid: true,
},
{
Name: "No expected scope always returns true",
HasScopes: map[string]struct{}{
"teams:*": {},
"permissions:*": {},
"users:*": {},
"permissions:delegate": {},
},
NeedAnyScope: []string{},
Valid: true,
},
{
Name: "Single scope from list",
HasScopes: map[string]struct{}{
"teams:1": {},
"permissions:delegate": {},
},
NeedAnyScope: []string{"teams:1", "permissions:delegate"},
Valid: true,
},
{
Name: "Single scope from glob list",
HasScopes: map[string]struct{}{
"teams:*": {},
"permissions:*": {},
"users:*": {},
"permissions:delegate": {},
},
NeedAnyScope: []string{"teams:1", "permissions:delegate"},
Valid: true,
},
{
Name: "Either of two scopes from glob list",
HasScopes: map[string]struct{}{
"teams:*": {},
"permissions:*": {},
"users:*": {},
"permissions:delegate": {},
},
NeedAnyScope: []string{"global:admin", "permissions:delegate"},
Valid: true,
},
{
Name: "No match found",
HasScopes: map[string]struct{}{
"teams:*": {},
"users:*": {},
"permissions:delegate": {},
},
NeedAnyScope: []string{"teams1", "permissions:nodelegate"},
Valid: false,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.Name, func(t *testing.T) {
ok, err := evaluateScope(tc.HasScopes, tc.NeedAnyScope...)
require.NoError(t, err)
assert.Equal(t, tc.Valid, ok)
})
}
}
@@ -0,0 +1,399 @@
package accesscontrol
import (
"testing"
"github.com/stretchr/testify/assert"
)
type evaluateTestCase struct {
desc string
expected bool
evaluator Evaluator
permissions map[string]map[string]struct{}
}
type injectTestCase struct {
desc string
expected bool
evaluator Evaluator
params map[string]string
permissions map[string]map[string]struct{}
}
func TestPermission_Evaluate(t *testing.T) {
tests := []evaluateTestCase{
{
desc: "should evaluate to true",
expected: true,
evaluator: EvalPermission("reports:read", "reports:1"),
permissions: map[string]map[string]struct{}{
"reports:read": {
"reports:1": struct{}{},
},
},
},
{
desc: "should evaluate to true when allEvaluator required scopes matches",
expected: true,
evaluator: EvalPermission("reports:read", "reports:1", "reports:2"),
permissions: map[string]map[string]struct{}{
"reports:read": {
"reports:1": struct{}{},
"reports:2": struct{}{},
},
},
},
{
desc: "should evaluate to true for empty scope",
expected: true,
evaluator: EvalPermission("reports:read"),
permissions: map[string]map[string]struct{}{
"reports:read": {
"reports:1": struct{}{},
},
},
},
{
desc: "should evaluate to false when only one of required scopes exists",
expected: false,
evaluator: EvalPermission("reports:read", "reports:1", "reports:2"),
permissions: map[string]map[string]struct{}{
"reports:read": {
"reports:1": struct{}{},
},
},
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
ok, err := test.evaluator.Evaluate(test.permissions)
assert.NoError(t, err)
assert.Equal(t, test.expected, ok)
})
}
}
func TestPermission_Inject(t *testing.T) {
tests := []injectTestCase{
{
desc: "should inject correct param",
expected: true,
evaluator: EvalPermission("reports:read", Scope("reports", Parameter(":reportId"))),
params: map[string]string{
":id": "10",
":reportId": "1",
},
permissions: map[string]map[string]struct{}{
"reports:read": {
"reports:1": struct{}{},
},
},
},
{
desc: "should fail for nil params",
expected: false,
evaluator: EvalPermission("reports:read", Scope("reports", Parameter(":reportId"))),
params: nil,
permissions: map[string]map[string]struct{}{
"reports:read": {
"reports:1": struct{}{},
},
},
},
{
desc: "should inject several parameters to one permission",
expected: true,
evaluator: EvalPermission("reports:read", Scope("reports", Parameter(":reportId"), Parameter(":reportId2"))),
params: map[string]string{
":reportId": "report",
":reportId2": "report2",
},
permissions: map[string]map[string]struct{}{
"reports:read": {
"reports:report:report2": struct{}{},
},
},
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
injected, err := test.evaluator.Inject(test.params)
assert.NoError(t, err)
ok, err := injected.Evaluate(test.permissions)
assert.NoError(t, err)
assert.Equal(t, test.expected, ok)
})
}
}
func TestAll_Evaluate(t *testing.T) {
tests := []evaluateTestCase{
{
desc: "should return true for one that matches",
evaluator: EvalAll(
EvalPermission("settings:write", Scope("settings", "*")),
),
permissions: map[string]map[string]struct{}{
"settings:write": {"settings:*": struct{}{}},
},
expected: true,
},
{
desc: "should return true for several that matches",
evaluator: EvalAll(
EvalPermission("settings:write", Scope("settings", "*")),
EvalPermission("settings:read", Scope("settings", "auth.saml", "*")),
),
permissions: map[string]map[string]struct{}{
"settings:write": {"settings:*": struct{}{}},
"settings:read": {"settings:*": struct{}{}},
},
expected: true,
},
{
desc: "should return false if one does not match",
evaluator: EvalAll(
EvalPermission("settings:write", Scope("settings", "*")),
EvalPermission("settings:read", Scope("settings", "auth.saml", "*")),
EvalPermission("report:read", Scope("reports", "*")),
),
permissions: map[string]map[string]struct{}{
"settings:write": {"settings:*": struct{}{}},
"settings:read": {"settings:*": struct{}{}},
"report:read": {"report:1": struct{}{}},
},
expected: false,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
ok, err := test.evaluator.Evaluate(test.permissions)
assert.NoError(t, err)
assert.Equal(t, test.expected, ok)
})
}
}
func TestAll_Inject(t *testing.T) {
tests := []injectTestCase{
{
desc: "should inject correct param",
expected: true,
evaluator: EvalAll(
EvalPermission("reports:read", Scope("reports", Parameter(":reportId"))),
EvalPermission("settings:read", Scope("settings", Parameter(":settingsId"))),
),
params: map[string]string{
":id": "10",
":settingsId": "3",
":reportId": "1",
},
permissions: map[string]map[string]struct{}{
"reports:read": {
"reports:1": struct{}{},
},
"settings:read": {
"settings:3": struct{}{},
},
},
},
{
desc: "should fail for nil params",
expected: false,
evaluator: EvalAll(
EvalPermission("settings:read", Scope("reports", Parameter(":settingsId"))),
EvalPermission("reports:read", Scope("reports", Parameter(":reportId"))),
),
params: nil,
permissions: map[string]map[string]struct{}{
"reports:read": {
"reports:1": struct{}{},
},
"settings:read": {
"settings:3": struct{}{},
},
},
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
injected, err := test.evaluator.Inject(test.params)
assert.NoError(t, err)
ok, err := injected.Evaluate(test.permissions)
assert.NoError(t, err)
assert.Equal(t, test.expected, ok)
})
}
}
func TestAny_Evaluate(t *testing.T) {
tests := []evaluateTestCase{
{
desc: "should return true for one that matches",
evaluator: EvalAny(
EvalPermission("settings:write", Scope("settings", "*")),
),
permissions: map[string]map[string]struct{}{
"settings:write": {"settings:*": struct{}{}},
},
expected: true,
},
{
desc: "should return true when at least one matches",
evaluator: EvalAny(
EvalPermission("settings:write", Scope("settings", "auth.saml", "*")),
EvalPermission("report:read", Scope("reports", "1")),
EvalPermission("report:write", Scope("reports", "10")),
),
permissions: map[string]map[string]struct{}{
"settings:write": {"settings:*": struct{}{}},
},
expected: true,
},
{
desc: "should return false when there is no match",
evaluator: EvalAny(
EvalPermission("settings:write", Scope("settings", "auth.saml", "*")),
EvalPermission("report:read", Scope("reports", "1")),
EvalPermission("report:write", Scope("reports", "10")),
),
permissions: map[string]map[string]struct{}{
"permissions:write": {"permissions:delegate": struct{}{}},
},
expected: false,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
ok, err := test.evaluator.Evaluate(test.permissions)
assert.NoError(t, err)
assert.Equal(t, test.expected, ok)
})
}
}
func TestAny_Inject(t *testing.T) {
tests := []injectTestCase{
{
desc: "should inject correct param",
expected: true,
evaluator: EvalAny(
EvalPermission("reports:read", Scope("reports", Parameter(":reportId"))),
EvalPermission("settings:read", Scope("settings", Parameter(":settingsId"))),
),
params: map[string]string{
":id": "10",
":settingsId": "3",
":reportId": "1",
},
permissions: map[string]map[string]struct{}{
"reports:read": {
"reports:1": struct{}{},
},
"settings:read": {
"settings:3": struct{}{},
},
},
},
{
desc: "should fail for nil params",
expected: false,
evaluator: EvalAny(
EvalPermission("settings:read", Scope("reports", Parameter(":settingsId"))),
EvalPermission("reports:read", Scope("reports", Parameter(":reportId"))),
),
params: nil,
permissions: map[string]map[string]struct{}{
"reports:read": {
"reports:1": struct{}{},
},
"settings:read": {
"settings:3": struct{}{},
},
},
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
injected, err := test.evaluator.Inject(test.params)
assert.NoError(t, err)
ok, err := injected.Evaluate(test.permissions)
assert.NoError(t, err)
assert.Equal(t, test.expected, ok)
})
}
}
type combinedTestCase struct {
desc string
evaluator Evaluator
expected bool
permissions map[string]map[string]struct{}
}
func TestEval(t *testing.T) {
tests := []combinedTestCase{
{
desc: "should return true when first is true",
evaluator: EvalAny(
EvalPermission("settings:write", Scope("settings", "*")),
EvalAll(
EvalPermission("settings:write", "settings:auth.saml:enabled"),
EvalPermission("settings:write", "settings:auth.saml:max_issue_delay"),
),
),
expected: true,
permissions: map[string]map[string]struct{}{
"settings:write": {"settings:*": struct{}{}},
},
},
{
desc: "should return true when first is false and all is true",
evaluator: EvalAny(
EvalPermission("settings:write", Scope("settings", "*")),
EvalAll(
EvalPermission("settings:write", "settings:auth.saml:enabled"),
EvalPermission("settings:write", "settings:auth.saml:max_issue_delay"),
),
),
expected: true,
permissions: map[string]map[string]struct{}{
"settings:write": {
"settings:auth.saml:enabled": struct{}{},
"settings:auth.saml:max_issue_delay": struct{}{},
},
},
},
{
desc: "should return false when both are false",
evaluator: EvalAny(
EvalPermission("settings:write", Scope("settings", "*")),
EvalAll(
EvalPermission("settings:write", "settings:auth.saml:enabled"),
EvalPermission("settings:write", "settings:auth.saml:max_issue_delay"),
),
),
expected: false,
permissions: map[string]map[string]struct{}{
"settings:write": {
"settings:auth.saml:enabled": struct{}{},
},
},
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
ok, err := test.evaluator.Evaluate(test.permissions)
assert.NoError(t, err)
assert.Equal(t, test.expected, ok)
})
}
}
@@ -1,68 +1,50 @@
package middleware
import (
"bytes"
"fmt"
"net/http"
"text/template"
"time"
"github.com/grafana/grafana/pkg/util"
macaron "gopkg.in/macaron.v1"
"gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/util"
)
func Middleware(ac accesscontrol.AccessControl) func(macaron.Handler, string, ...string) macaron.Handler {
return func(fallback macaron.Handler, permission string, scopes ...string) macaron.Handler {
func Middleware(ac accesscontrol.AccessControl) func(macaron.Handler, accesscontrol.Evaluator) macaron.Handler {
return func(fallback macaron.Handler, evaluator accesscontrol.Evaluator) macaron.Handler {
if ac.IsDisabled() {
return fallback
}
return func(c *models.ReqContext) {
// We need this otherwise templated scopes get initialized only once, during the first call
runtimeScope := make([]string, len(scopes))
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
}
runtimeScope[i] = buf.String()
}
hasAccess, err := ac.Evaluate(c.Req.Context(), c.SignedInUser, permission, runtimeScope...)
injected, err := evaluator.Inject(c.AllParams())
if err != nil {
Deny(c, permission, runtimeScope, err)
c.JsonApiErr(http.StatusInternalServerError, "Internal server error", err)
return
}
if !hasAccess {
Deny(c, permission, runtimeScope, nil)
hasAccess, err := ac.Evaluate(c.Req.Context(), c.SignedInUser, injected)
if !hasAccess || err != nil {
Deny(c, injected, err)
return
}
}
}
}
func Deny(c *models.ReqContext, permission string, scopes []string, err error) {
func Deny(c *models.ReqContext, evaluator accesscontrol.Evaluator, err error) {
id := newID()
if err != nil {
c.Logger.Error("Error from access control system", "error", err, "accessErrorID", id)
} else {
c.Logger.Info("Access denied",
c.Logger.Info(
"Access denied",
"userID", c.UserId,
"permission", permission,
"scopes", scopes,
"accessErrorID", id)
"accessErrorID", id,
"permissions", evaluator.String(),
)
}
// If the user triggers an error in the access control system, we
@@ -0,0 +1,121 @@
package middleware
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"gopkg.in/macaron.v1"
"github.com/stretchr/testify/assert"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
)
type middlewareTestCase struct {
desc string
expectFallback bool
expectEndpoint bool
evaluator accesscontrol.Evaluator
ac accesscontrol.AccessControl
}
func TestMiddleware(t *testing.T) {
tests := []middlewareTestCase{
{
desc: "should use fallback if access control is disabled",
ac: fakeAccessControl{isDisabled: true},
expectFallback: true,
expectEndpoint: true,
},
{
desc: "should pass middleware for correct permissions",
ac: fakeAccessControl{
isDisabled: false,
permissions: []*accesscontrol.Permission{{Action: "users:read", Scope: "users:*"}},
},
evaluator: accesscontrol.EvalPermission("users:read", "users:*"),
expectFallback: false,
expectEndpoint: true,
},
{
desc: "should not reach endpoint when missing permissions",
ac: fakeAccessControl{
isDisabled: false,
permissions: []*accesscontrol.Permission{{Action: "users:read", Scope: "users:1"}},
},
evaluator: accesscontrol.EvalPermission("users:read", "users:*"),
expectFallback: false,
expectEndpoint: false,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
fallbackCalled := false
fallback := func(c *models.ReqContext) {
fallbackCalled = true
}
server := macaron.New()
server.UseMiddleware(macaron.Renderer("../../public/views", "[[", "]]"))
server.Use(contextProvider())
server.Use(Middleware(test.ac)(fallback, test.evaluator))
endpointCalled := false
server.Get("/", func(c *models.ReqContext) {
endpointCalled = true
})
request, err := http.NewRequest(http.MethodGet, "/", nil)
assert.NoError(t, err)
recorder := httptest.NewRecorder()
server.ServeHTTP(recorder, request)
assert.Equal(t, test.expectFallback, fallbackCalled)
assert.Equal(t, test.expectEndpoint, endpointCalled)
})
}
}
func contextProvider() macaron.Handler {
return func(c *macaron.Context) {
reqCtx := &models.ReqContext{
Context: c,
Logger: log.New(""),
SignedInUser: &models.SignedInUser{},
IsSignedIn: true,
SkipCache: true,
}
c.Map(reqCtx)
}
}
var _ accesscontrol.AccessControl = new(fakeAccessControl)
type fakeAccessControl struct {
isDisabled bool
permissions []*accesscontrol.Permission
}
func (f fakeAccessControl) Evaluate(ctx context.Context, user *models.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) {
permissions, _ := f.GetUserPermissions(ctx, user)
return evaluator.Evaluate(accesscontrol.GroupScopesByAction(permissions))
}
func (f fakeAccessControl) GetUserPermissions(ctx context.Context, user *models.SignedInUser) ([]*accesscontrol.Permission, error) {
return f.permissions, nil
}
func (f fakeAccessControl) IsDisabled() bool {
return f.isDisabled
}
func (f fakeAccessControl) DeclareFixedRoles(registration ...accesscontrol.RoleRegistration) error {
return nil
}
+3 -8
View File
@@ -34,11 +34,6 @@ type Permission struct {
Scope string `json:"scope"`
}
type EvaluationResult struct {
HasAccess bool
Meta interface{}
}
func (p RoleDTO) Role() Role {
return Role{
Name: p.Name,
@@ -53,13 +48,13 @@ const (
ActionUsersRead = "users:read"
ActionUsersWrite = "users:write"
ActionUsersTeamRead = "users.teams:read"
// We can ignore gosec G101 since this does not contain any credentials
// We can ignore gosec G101 since this does not contain any credentials.
// nolint:gosec
ActionUsersAuthTokenList = "users.authtoken:list"
// We can ignore gosec G101 since this does not contain any credentials
// We can ignore gosec G101 since this does not contain any credentials.
// nolint:gosec
ActionUsersAuthTokenUpdate = "users.authtoken:update"
// We can ignore gosec G101 since this does not contain any credentials
// We can ignore gosec G101 since this does not contain any credentials.
// nolint:gosec
ActionUsersPasswordUpdate = "users.password:update"
ActionUsersDelete = "users:delete"
@@ -8,7 +8,6 @@ import (
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/evaluator"
"github.com/grafana/grafana/pkg/setting"
"github.com/prometheus/client_golang/prometheus"
)
@@ -55,9 +54,17 @@ func (ac *OSSAccessControlService) getUsageMetrics() interface{} {
return 1
}
// Evaluate evaluates access to the given resource
func (ac *OSSAccessControlService) Evaluate(ctx context.Context, user *models.SignedInUser, permission string, scope ...string) (bool, error) {
return evaluator.Evaluate(ctx, ac, user, permission, scope...)
// Evaluate evaluates access to the given resources
func (ac *OSSAccessControlService) Evaluate(ctx context.Context, user *models.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) {
timer := prometheus.NewTimer(metrics.MAccessEvaluationsSummary)
defer timer.ObserveDuration()
metrics.MAccessEvaluationCount.Inc()
permissions, err := ac.GetUserPermissions(ctx, user)
if err != nil {
return false, err
}
return evaluator.Evaluate(accesscontrol.GroupScopesByAction(permissions))
}
// GetUserPermissions returns user permissions based on built-in roles
@@ -94,8 +94,7 @@ type userTestCase struct {
}
type endpointTestCase struct {
permission string
scope []string
evaluator accesscontrol.Evaluator
}
func TestEvaluatingPermissions(t *testing.T) {
@@ -108,8 +107,8 @@ func TestEvaluatingPermissions(t *testing.T) {
isGrafanaAdmin: false,
},
endpoints: []endpointTestCase{
{permission: accesscontrol.ActionUsersDisable, scope: []string{accesscontrol.ScopeGlobalUsersAll}},
{permission: accesscontrol.ActionUsersEnable, scope: []string{accesscontrol.ScopeGlobalUsersAll}},
{evaluator: accesscontrol.EvalPermission(accesscontrol.ActionUsersDisable, accesscontrol.ScopeGlobalUsersAll)},
{evaluator: accesscontrol.EvalPermission(accesscontrol.ActionUsersEnable, accesscontrol.ScopeGlobalUsersAll)},
},
evalResult: true,
},
@@ -121,7 +120,7 @@ func TestEvaluatingPermissions(t *testing.T) {
isGrafanaAdmin: false,
},
endpoints: []endpointTestCase{
{permission: accesscontrol.ActionUsersCreate, scope: []string{accesscontrol.ScopeGlobalUsersAll}},
{evaluator: accesscontrol.EvalPermission(accesscontrol.ActionUsersCreate, accesscontrol.ScopeGlobalUsersAll)},
},
evalResult: false,
},
@@ -140,7 +139,7 @@ func TestEvaluatingPermissions(t *testing.T) {
}
for _, endpoint := range tc.endpoints {
result, err := ac.Evaluate(context.Background(), user, endpoint.permission, endpoint.scope...)
result, err := ac.Evaluate(context.Background(), user, endpoint.evaluator)
require.NoError(t, err)
assert.Equal(t, tc.evalResult, result)
}
+25
View File
@@ -0,0 +1,25 @@
package accesscontrol
import (
"fmt"
"strings"
)
// Scope builds scope from parts
// e.g. Scope("users", "*") return "users:*"
func Scope(parts ...string) string {
b := strings.Builder{}
for i, c := range parts {
if i != 0 {
b.WriteRune(':')
}
b.WriteString(c)
}
return b.String()
}
// Parameter returns injectable scope part
// e.g. Scope("users", Parameter(":id")) or "users:" + Parameter(":id")
func Parameter(key string) string {
return fmt.Sprintf(`{{ index . "%s" }}`, key)
}