mirror of https://github.com/grafana/grafana
Alerting: Update provisioning API to support regular permissions (#77007)
* allow users with regular actions access provisioning API paths * update methods that read rules skip new authorization logic if user CanReadAllRules to avoid performance impact on file-provisioning update all methods to accept identity.Requester that contains all permissions and is required by access control. * create deltas for single rul e * update modify methods skip new authorization logic if user CanWriteAllRules to avoid performance impact on file-provisioning update all methods to accept identity.Requester that contains all permissions and is required by access control. * implement RuleAccessControlService in provisioning * update file provisioning user to have all permissions to bypass authz * update provisioning API to return errutil errors correctly --------- Co-authored-by: Alexander Weaver <weaver.alex.d@gmail.com>pull/83295/head^2
parent
0b4830ccfd
commit
b9abb8cabb
@ -0,0 +1,76 @@ |
||||
package provisioning |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol" |
||||
"github.com/grafana/grafana/pkg/services/auth/identity" |
||||
"github.com/grafana/grafana/pkg/services/ngalert/models" |
||||
"github.com/grafana/grafana/pkg/services/ngalert/store" |
||||
) |
||||
|
||||
type RuleAccessControlService interface { |
||||
HasAccess(ctx context.Context, user identity.Requester, evaluator ac.Evaluator) (bool, error) |
||||
AuthorizeAccessToRuleGroup(ctx context.Context, user identity.Requester, rules models.RulesGroup) error |
||||
AuthorizeRuleChanges(ctx context.Context, user identity.Requester, change *store.GroupDelta) error |
||||
} |
||||
|
||||
func newRuleAccessControlService(ac RuleAccessControlService) *provisioningRuleAccessControl { |
||||
return &provisioningRuleAccessControl{ |
||||
RuleAccessControlService: ac, |
||||
} |
||||
} |
||||
|
||||
type provisioningRuleAccessControl struct { |
||||
RuleAccessControlService |
||||
} |
||||
|
||||
var _ ruleAccessControlService = &provisioningRuleAccessControl{} |
||||
|
||||
// AuthorizeRuleGroupRead authorizes the read access to a group of rules for a user.
|
||||
// It first checks if the user has permission to read all rules. If yes, it bypasses the authorization.
|
||||
// If not, it calls the RuleAccessControlService to authorize access to the rule group.
|
||||
// It returns an error if the authorization fails or if there is an error during permission check.
|
||||
func (p *provisioningRuleAccessControl) AuthorizeRuleGroupRead(ctx context.Context, user identity.Requester, rules models.RulesGroup) error { |
||||
can, err := p.CanReadAllRules(ctx, user) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if !can { |
||||
return p.RuleAccessControlService.AuthorizeAccessToRuleGroup(ctx, user, rules) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// AuthorizeRuleGroupWrite authorizes the write access to a group of rules for a user.
|
||||
// It first checks if the user has permission to write all rules. If yes, it bypasses the authorization.
|
||||
// If not, it calls the RuleAccessControlService to authorize the rule changes.
|
||||
// It returns an error if the authorization fails or if there is an error during permission check.
|
||||
func (p *provisioningRuleAccessControl) AuthorizeRuleGroupWrite(ctx context.Context, user identity.Requester, change *store.GroupDelta) error { |
||||
can, err := p.CanWriteAllRules(ctx, user) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if !can { |
||||
return p.RuleAccessControlService.AuthorizeRuleChanges(ctx, user, change) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// CanReadAllRules checks if the user has permission to read all rules.
|
||||
// It evaluates if the user has either "alert.provisioning:read" or "alert.provisioning.secrets:read" permissions.
|
||||
// It returns true if the user has the required permissions, otherwise it returns false.
|
||||
func (p *provisioningRuleAccessControl) CanReadAllRules(ctx context.Context, user identity.Requester) (bool, error) { |
||||
return p.HasAccess(ctx, user, ac.EvalAny( |
||||
ac.EvalPermission(ac.ActionAlertingProvisioningRead), |
||||
ac.EvalPermission(ac.ActionAlertingProvisioningReadSecrets), |
||||
)) |
||||
} |
||||
|
||||
// CanWriteAllRules is a method that checks if a user has permission to write all rules.
|
||||
// It calls the HasAccess method with the provided action "alert.provisioning:write".
|
||||
// It returns true if the user has permission, false otherwise.
|
||||
// It returns an error if there is a problem checking the permission.
|
||||
func (p *provisioningRuleAccessControl) CanWriteAllRules(ctx context.Context, user identity.Requester) (bool, error) { |
||||
return p.HasAccess(ctx, user, ac.EvalPermission(ac.ActionAlertingProvisioningWrite)) |
||||
} |
||||
@ -0,0 +1,231 @@ |
||||
package provisioning |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
"github.com/stretchr/testify/require" |
||||
"golang.org/x/exp/rand" |
||||
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol" |
||||
"github.com/grafana/grafana/pkg/services/auth/identity" |
||||
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol/fakes" |
||||
"github.com/grafana/grafana/pkg/services/ngalert/models" |
||||
"github.com/grafana/grafana/pkg/services/ngalert/store" |
||||
"github.com/grafana/grafana/pkg/services/user" |
||||
) |
||||
|
||||
func TestCanReadAllRules(t *testing.T) { |
||||
testUser := &user.SignedInUser{} |
||||
|
||||
t.Run("should check for provisioning permissions", func(t *testing.T) { |
||||
rs := &fakes.FakeRuleService{} |
||||
expected := rand.Int()%2 == 1 |
||||
rs.HasAccessFunc = func(ctx context.Context, requester identity.Requester, evaluator accesscontrol.Evaluator) (bool, error) { |
||||
return expected, nil |
||||
} |
||||
p := &provisioningRuleAccessControl{rs} |
||||
res, err := p.CanReadAllRules(context.Background(), testUser) |
||||
require.NoError(t, err) |
||||
require.Equal(t, expected, res) |
||||
|
||||
require.Len(t, rs.Calls, 1) |
||||
require.Equal(t, "HasAccess", rs.Calls[0].MethodName) |
||||
require.Equal(t, accesscontrol.EvalAny( |
||||
accesscontrol.EvalPermission(accesscontrol.ActionAlertingProvisioningRead), |
||||
accesscontrol.EvalPermission(accesscontrol.ActionAlertingProvisioningReadSecrets), |
||||
).GoString(), rs.Calls[0].Arguments[2].(accesscontrol.Evaluator).GoString()) |
||||
}) |
||||
|
||||
t.Run("should return error", func(t *testing.T) { |
||||
rs := &fakes.FakeRuleService{} |
||||
expected := errors.New("test") |
||||
rs.HasAccessFunc = func(ctx context.Context, requester identity.Requester, evaluator accesscontrol.Evaluator) (bool, error) { |
||||
return false, expected |
||||
} |
||||
p := &provisioningRuleAccessControl{rs} |
||||
_, err := p.CanReadAllRules(context.Background(), testUser) |
||||
require.ErrorIs(t, err, expected) |
||||
}) |
||||
} |
||||
|
||||
func TestCanWriteAllRules(t *testing.T) { |
||||
testUser := &user.SignedInUser{} |
||||
|
||||
t.Run("should check for provisioning permissions", func(t *testing.T) { |
||||
rs := &fakes.FakeRuleService{} |
||||
expected := rand.Int()%2 == 1 |
||||
rs.HasAccessFunc = func(ctx context.Context, requester identity.Requester, evaluator accesscontrol.Evaluator) (bool, error) { |
||||
return expected, nil |
||||
} |
||||
p := &provisioningRuleAccessControl{rs} |
||||
res, err := p.CanWriteAllRules(context.Background(), testUser) |
||||
require.NoError(t, err) |
||||
require.Equal(t, expected, res) |
||||
|
||||
require.Len(t, rs.Calls, 1) |
||||
require.Equal(t, "HasAccess", rs.Calls[0].MethodName) |
||||
require.Equal(t, accesscontrol.EvalPermission(accesscontrol.ActionAlertingProvisioningWrite).GoString(), rs.Calls[0].Arguments[2].(accesscontrol.Evaluator).GoString()) |
||||
}) |
||||
|
||||
t.Run("should return error", func(t *testing.T) { |
||||
rs := &fakes.FakeRuleService{} |
||||
expected := errors.New("test") |
||||
rs.HasAccessFunc = func(ctx context.Context, requester identity.Requester, evaluator accesscontrol.Evaluator) (bool, error) { |
||||
return false, expected |
||||
} |
||||
p := &provisioningRuleAccessControl{rs} |
||||
_, err := p.CanWriteAllRules(context.Background(), testUser) |
||||
require.ErrorIs(t, err, expected) |
||||
}) |
||||
} |
||||
|
||||
func TestAuthorizeAccessToRuleGroup(t *testing.T) { |
||||
testUser := &user.SignedInUser{} |
||||
rules := models.GenerateAlertRules(1, models.AlertRuleGen()) |
||||
|
||||
t.Run("should return nil when user has provisioning permissions", func(t *testing.T) { |
||||
rs := &fakes.FakeRuleService{} |
||||
provisioner := provisioningRuleAccessControl{ |
||||
RuleAccessControlService: rs, |
||||
} |
||||
|
||||
rs.HasAccessFunc = func(ctx context.Context, user identity.Requester, evaluator accesscontrol.Evaluator) (bool, error) { |
||||
return true, nil |
||||
} |
||||
|
||||
err := provisioner.AuthorizeRuleGroupRead(context.Background(), testUser, rules) |
||||
require.NoError(t, err) |
||||
|
||||
require.Len(t, rs.Calls, 1) |
||||
require.Equal(t, "HasAccess", rs.Calls[0].MethodName) |
||||
assert.Equal(t, accesscontrol.EvalAny( |
||||
accesscontrol.EvalPermission(accesscontrol.ActionAlertingProvisioningRead), |
||||
accesscontrol.EvalPermission(accesscontrol.ActionAlertingProvisioningReadSecrets), |
||||
).GoString(), rs.Calls[0].Arguments[2].(accesscontrol.Evaluator).GoString()) |
||||
assert.Equal(t, testUser, rs.Calls[0].Arguments[1]) |
||||
}) |
||||
|
||||
t.Run("should call upstream method if no provisioning permissions", func(t *testing.T) { |
||||
rs := &fakes.FakeRuleService{} |
||||
provisioner := provisioningRuleAccessControl{ |
||||
RuleAccessControlService: rs, |
||||
} |
||||
|
||||
rs.HasAccessFunc = func(ctx context.Context, user identity.Requester, evaluator accesscontrol.Evaluator) (bool, error) { |
||||
return false, nil |
||||
} |
||||
rs.AuthorizeAccessToRuleGroupFunc = func(ctx context.Context, requester identity.Requester, group models.RulesGroup) error { |
||||
return nil |
||||
} |
||||
|
||||
err := provisioner.AuthorizeRuleGroupRead(context.Background(), testUser, rules) |
||||
require.NoError(t, err) |
||||
|
||||
require.Len(t, rs.Calls, 2) |
||||
require.Equal(t, "HasAccess", rs.Calls[0].MethodName) |
||||
require.Equal(t, "AuthorizeRuleGroupRead", rs.Calls[1].MethodName) |
||||
require.Equal(t, models.RulesGroup(rules), rs.Calls[1].Arguments[2]) |
||||
}) |
||||
|
||||
t.Run("should propagate error", func(t *testing.T) { |
||||
rs := &fakes.FakeRuleService{} |
||||
provisioner := provisioningRuleAccessControl{ |
||||
RuleAccessControlService: rs, |
||||
} |
||||
|
||||
expected := errors.New("test1") |
||||
rs.HasAccessFunc = func(ctx context.Context, user identity.Requester, evaluator accesscontrol.Evaluator) (bool, error) { |
||||
return false, expected |
||||
} |
||||
|
||||
err := provisioner.AuthorizeRuleGroupRead(context.Background(), testUser, rules) |
||||
require.ErrorIs(t, err, expected) |
||||
|
||||
rs.HasAccessFunc = func(ctx context.Context, user identity.Requester, evaluator accesscontrol.Evaluator) (bool, error) { |
||||
return false, nil |
||||
} |
||||
expected = errors.New("test2") |
||||
rs.AuthorizeAccessToRuleGroupFunc = func(ctx context.Context, requester identity.Requester, group models.RulesGroup) error { |
||||
return expected |
||||
} |
||||
|
||||
err = provisioner.AuthorizeRuleGroupRead(context.Background(), testUser, rules) |
||||
require.ErrorIs(t, err, expected) |
||||
}) |
||||
} |
||||
|
||||
func TestAuthorizeRuleChanges(t *testing.T) { |
||||
testUser := &user.SignedInUser{} |
||||
change := &store.GroupDelta{} |
||||
|
||||
t.Run("should return nil when user has provisioning permissions", func(t *testing.T) { |
||||
rs := &fakes.FakeRuleService{} |
||||
provisioner := provisioningRuleAccessControl{ |
||||
RuleAccessControlService: rs, |
||||
} |
||||
|
||||
rs.HasAccessFunc = func(ctx context.Context, user identity.Requester, evaluator accesscontrol.Evaluator) (bool, error) { |
||||
return true, nil |
||||
} |
||||
|
||||
err := provisioner.AuthorizeRuleGroupWrite(context.Background(), testUser, change) |
||||
require.NoError(t, err) |
||||
|
||||
require.Len(t, rs.Calls, 1) |
||||
require.Equal(t, "HasAccess", rs.Calls[0].MethodName) |
||||
assert.Equal(t, accesscontrol.EvalPermission(accesscontrol.ActionAlertingProvisioningWrite).GoString(), rs.Calls[0].Arguments[2].(accesscontrol.Evaluator).GoString()) |
||||
assert.Equal(t, testUser, rs.Calls[0].Arguments[1]) |
||||
}) |
||||
|
||||
t.Run("should call upstream method if no provisioning permissions", func(t *testing.T) { |
||||
rs := &fakes.FakeRuleService{} |
||||
provisioner := provisioningRuleAccessControl{ |
||||
RuleAccessControlService: rs, |
||||
} |
||||
|
||||
rs.HasAccessFunc = func(ctx context.Context, user identity.Requester, evaluator accesscontrol.Evaluator) (bool, error) { |
||||
return false, nil |
||||
} |
||||
rs.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, delta *store.GroupDelta) error { |
||||
return nil |
||||
} |
||||
|
||||
err := provisioner.AuthorizeRuleGroupWrite(context.Background(), testUser, change) |
||||
require.NoError(t, err) |
||||
|
||||
require.Len(t, rs.Calls, 2) |
||||
require.Equal(t, "HasAccess", rs.Calls[0].MethodName) |
||||
require.Equal(t, "AuthorizeRuleGroupWrite", rs.Calls[1].MethodName) |
||||
require.Equal(t, testUser, rs.Calls[1].Arguments[1]) |
||||
require.Equal(t, change, rs.Calls[1].Arguments[2]) |
||||
}) |
||||
|
||||
t.Run("should propagate error", func(t *testing.T) { |
||||
rs := &fakes.FakeRuleService{} |
||||
provisioner := provisioningRuleAccessControl{ |
||||
RuleAccessControlService: rs, |
||||
} |
||||
|
||||
expected := errors.New("test1") |
||||
rs.HasAccessFunc = func(ctx context.Context, user identity.Requester, evaluator accesscontrol.Evaluator) (bool, error) { |
||||
return false, expected |
||||
} |
||||
|
||||
err := provisioner.AuthorizeRuleGroupWrite(context.Background(), testUser, change) |
||||
require.ErrorIs(t, err, expected) |
||||
|
||||
rs.HasAccessFunc = func(ctx context.Context, user identity.Requester, evaluator accesscontrol.Evaluator) (bool, error) { |
||||
return false, nil |
||||
} |
||||
expected = errors.New("test2") |
||||
rs.AuthorizeRuleChangesFunc = func(ctx context.Context, requester identity.Requester, delta *store.GroupDelta) error { |
||||
return expected |
||||
} |
||||
|
||||
err = provisioner.AuthorizeRuleGroupWrite(context.Background(), testUser, change) |
||||
require.ErrorIs(t, err, expected) |
||||
}) |
||||
} |
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue