grafana/pkg/services/ngalert/api/authorization_test.go
Alexander Weaver 9af30f6570
Alerting: Provisioning GET routes for mute timings (#49044)
* Define GET routes and run codegen

* Wire up forked and non-generated API

* Implement and wire

* Tests, authorization

* Fix linter error
2022-05-17 13:42:48 -05:00

427 lines
12 KiB
Go

package api
import (
"math/rand"
"net/http"
"os"
"path/filepath"
"testing"
"github.com/go-openapi/loads"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/expr"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/ngalert/models"
)
func TestAuthorize(t *testing.T) {
json, err := os.ReadFile(filepath.Join("tooling", "spec.json"))
require.NoError(t, err)
swaggerSpec, err := loads.Analyzed(json, "")
require.NoError(t, err)
paths := make(map[string][]string)
for p, item := range swaggerSpec.Spec().Paths.Paths {
var methods []string
if item.Get != nil {
methods = append(methods, http.MethodGet)
}
if item.Put != nil {
methods = append(methods, http.MethodPut)
}
if item.Post != nil {
methods = append(methods, http.MethodPost)
}
if item.Delete != nil {
methods = append(methods, http.MethodDelete)
}
if item.Patch != nil {
methods = append(methods, http.MethodPatch)
}
paths[p] = methods
}
require.Len(t, paths, 36)
ac := acmock.New()
api := &API{AccessControl: ac}
t.Run("should not panic on known routes", func(t *testing.T) {
for path, methods := range paths {
for _, method := range methods {
require.NotPanics(t, func() {
api.authorize(method, path)
})
}
}
})
t.Run("should panic if route is unknown", func(t *testing.T) {
require.Panics(t, func() {
api.authorize("test", "test")
})
})
}
func TestAuthorizeRuleChanges(t *testing.T) {
groupKey := models.GenerateGroupKey(rand.Int63())
namespaceIdScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(groupKey.NamespaceUID)
testCases := []struct {
name string
changes func() *changes
permissions func(c *changes) map[string][]string
}{
{
name: "if there are rules to add it should check create action and query for datasource",
changes: func() *changes {
return &changes{
GroupKey: groupKey,
New: models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(withGroupKey(groupKey))),
Update: nil,
Delete: nil,
}
},
permissions: func(c *changes) map[string][]string {
var scopes []string
for _, rule := range c.New {
for _, query := range rule.Data {
scopes = append(scopes, datasources.ScopeProvider.GetResourceScopeUID(query.DatasourceUID))
}
}
return map[string][]string{
ac.ActionAlertingRuleCreate: {
namespaceIdScope,
},
datasources.ActionQuery: scopes,
}
},
},
{
name: "if there are rules to update within the same namespace it should check update action",
changes: func() *changes {
rules := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(withGroupKey(groupKey)))
updates := make([]ruleUpdate, 0, len(rules))
for _, rule := range rules {
updates = append(updates, ruleUpdate{
Existing: rule,
New: models.CopyRule(rule),
Diff: nil,
})
}
return &changes{
GroupKey: groupKey,
New: nil,
Update: updates,
Delete: nil,
}
},
permissions: func(c *changes) map[string][]string {
var scopes []string
for _, update := range c.Update {
for _, query := range update.New.Data {
scopes = append(scopes, datasources.ScopeProvider.GetResourceScopeUID(query.DatasourceUID))
}
}
return map[string][]string{
ac.ActionAlertingRuleUpdate: {
namespaceIdScope,
},
datasources.ActionQuery: scopes,
}
},
},
{
name: "if there are rules that are moved between namespaces it should check update action",
changes: func() *changes {
rules := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(withGroupKey(groupKey)))
updates := make([]ruleUpdate, 0, len(rules))
for _, rule := range rules {
cp := models.CopyRule(rule)
cp.NamespaceUID = rule.NamespaceUID + "other"
updates = append(updates, ruleUpdate{
Existing: cp,
New: rule,
Diff: nil,
})
}
return &changes{
GroupKey: groupKey,
New: nil,
Update: updates,
Delete: nil,
}
},
permissions: func(c *changes) map[string][]string {
var scopes []string
for _, update := range c.Update {
for _, query := range update.New.Data {
scopes = append(scopes, datasources.ScopeProvider.GetResourceScopeUID(query.DatasourceUID))
}
}
return map[string][]string{
ac.ActionAlertingRuleDelete: {
dashboards.ScopeFoldersProvider.GetResourceScopeUID(groupKey.NamespaceUID + "other"),
},
ac.ActionAlertingRuleCreate: {
namespaceIdScope,
},
datasources.ActionQuery: scopes,
}
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
executed := false
groupChanges := testCase.changes()
result, err := authorizeRuleChanges(groupChanges, func(evaluator ac.Evaluator) bool {
response, err := evaluator.Evaluate(make(map[string][]string))
require.False(t, response)
require.NoError(t, err)
executed = true
return false
})
require.Nil(t, result)
require.Error(t, err)
require.Truef(t, executed, "evaluation function is expected to be called but it was not.")
permissions := testCase.permissions(groupChanges)
executed = false
result, err = authorizeRuleChanges(groupChanges, func(evaluator ac.Evaluator) bool {
response, err := evaluator.Evaluate(permissions)
require.Truef(t, response, "provided permissions [%v] is not enough for requested permissions [%s]", testCase.permissions, evaluator.GoString())
require.NoError(t, err)
executed = true
return true
})
require.NoError(t, err)
require.Equal(t, groupChanges, result)
require.Truef(t, executed, "evaluation function is expected to be called but it was not.")
})
}
}
func TestAuthorizeRuleDelete(t *testing.T) {
groupKey := models.GenerateGroupKey(rand.Int63())
namespaceScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(groupKey.NamespaceUID)
getScopes := func(rules []*models.AlertRule) []string {
var scopes []string
for _, rule := range rules {
for _, query := range rule.Data {
scopes = append(scopes, datasources.ScopeProvider.GetResourceScopeUID(query.DatasourceUID))
}
}
return scopes
}
testCases := []struct {
name string
changes func() *changes
permissions func(c *changes) map[string][]string
assert func(t *testing.T, orig, authz *changes, err error)
}{
{
name: "should validate check access to data source and folder",
changes: func() *changes {
return &changes{
GroupKey: groupKey,
New: nil,
Update: nil,
Delete: models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withGroupKey(groupKey))),
}
},
permissions: func(c *changes) map[string][]string {
return map[string][]string{
ac.ActionAlertingRuleDelete: {
namespaceScope,
},
datasources.ActionQuery: getScopes(c.Delete),
}
},
assert: func(t *testing.T, orig, authz *changes, err error) {
require.NoError(t, err)
require.Equal(t, orig, authz)
},
},
{
name: "should remove rules user does not have access to data source",
changes: func() *changes {
return &changes{
GroupKey: groupKey,
New: nil,
Update: nil,
Delete: models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withGroupKey(groupKey))),
}
},
permissions: func(c *changes) map[string][]string {
return map[string][]string{
ac.ActionAlertingRuleDelete: {
namespaceScope,
},
datasources.ActionQuery: {
getScopes(c.Delete[:1])[0],
},
}
},
assert: func(t *testing.T, orig, authz *changes, err error) {
require.NoError(t, err)
require.Greater(t, len(orig.Delete), len(authz.Delete))
},
},
{
name: "should not fail if no changes other than unauthorized",
changes: func() *changes {
return &changes{
GroupKey: groupKey,
New: nil,
Update: nil,
Delete: models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withGroupKey(groupKey))),
}
},
permissions: func(c *changes) map[string][]string {
return map[string][]string{
ac.ActionAlertingRuleDelete: {
namespaceScope,
},
}
},
assert: func(t *testing.T, orig, authz *changes, err error) {
require.NoError(t, err)
require.False(t, orig.isEmpty())
require.True(t, authz.isEmpty())
},
},
{
name: "should not fail if there are changes and no rules can be deleted",
changes: func() *changes {
return &changes{
GroupKey: groupKey,
New: models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withGroupKey(groupKey))),
Update: nil,
Delete: models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withGroupKey(groupKey))),
}
},
permissions: func(c *changes) map[string][]string {
return map[string][]string{
ac.ActionAlertingRuleDelete: {
namespaceScope,
},
ac.ActionAlertingRuleCreate: {
namespaceScope,
},
datasources.ActionQuery: getScopes(c.New),
}
},
assert: func(t *testing.T, _, c *changes, err error) {
require.NoError(t, err)
require.Empty(t, c.Delete)
},
},
{
name: "should fail if no access to folder",
changes: func() *changes {
return &changes{
GroupKey: groupKey,
New: nil,
Update: nil,
Delete: models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withGroupKey(groupKey))),
}
},
permissions: func(c *changes) map[string][]string {
return map[string][]string{
datasources.ActionQuery: getScopes(c.Delete),
}
},
assert: func(t *testing.T, _, c *changes, err error) {
require.ErrorIs(t, err, ErrAuthorization)
require.Nil(t, c)
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
groupChanges := testCase.changes()
permissions := testCase.permissions(groupChanges)
result, err := authorizeRuleChanges(groupChanges, func(evaluator ac.Evaluator) bool {
response, err := evaluator.Evaluate(permissions)
require.NoError(t, err)
return response
})
testCase.assert(t, groupChanges, result, err)
})
}
}
func TestCheckDatasourcePermissionsForRule(t *testing.T) {
rule := models.AlertRuleGen()()
expressionByType := models.GenerateAlertQuery()
expressionByType.QueryType = expr.DatasourceType
expressionByUID := models.GenerateAlertQuery()
expressionByUID.DatasourceUID = expr.OldDatasourceUID
var data []models.AlertQuery
var scopes []string
expectedExecutions := rand.Intn(3) + 2
for i := 0; i < expectedExecutions; i++ {
q := models.GenerateAlertQuery()
scopes = append(scopes, datasources.ScopeProvider.GetResourceScopeUID(q.DatasourceUID))
data = append(data, q)
}
data = append(data, expressionByType, expressionByUID)
rand.Shuffle(len(data), func(i, j int) {
data[j], data[i] = data[i], data[j]
})
rule.Data = data
t.Run("should check only expressions", func(t *testing.T) {
permissions := map[string][]string{
datasources.ActionQuery: scopes,
}
executed := 0
eval := authorizeDatasourceAccessForRule(rule, func(evaluator ac.Evaluator) bool {
response, err := evaluator.Evaluate(permissions)
require.Truef(t, response, "provided permissions [%v] is not enough for requested permissions [%s]", permissions, evaluator.GoString())
require.NoError(t, err)
executed++
return true
})
require.True(t, eval)
require.Equal(t, expectedExecutions, executed)
})
t.Run("should return on first negative evaluation", func(t *testing.T) {
executed := 0
eval := authorizeDatasourceAccessForRule(rule, func(evaluator ac.Evaluator) bool {
executed++
return false
})
require.False(t, eval)
require.Equal(t, 1, executed)
})
}