RBAC: Add legacy authorization checks to service accounts (#93753)

* Extract a helper funtion to perform list with authorization checks

* Add k8s verb to utils package

* Construct default mapping when no custom mapping is passed

* Configure authorization checks for service accounts

* Fix helper and add filtering to service accounts
This commit is contained in:
Karl Persson
2024-09-27 15:53:11 +02:00
committed by GitHub
parent 7710f1c3cf
commit 0160f4f72c
14 changed files with 424 additions and 118 deletions
+94
View File
@@ -1,9 +1,14 @@
package common
import (
"context"
"strconv"
"github.com/grafana/authlib/claims"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/team"
)
@@ -23,3 +28,92 @@ func MapTeamPermission(p team.PermissionType) iamv0.TeamPermission {
return iamv0.TeamPermissionMember
}
}
// Resource is required to be implemented for list return types so we can
// perform authorization.
type Resource interface {
AuthID() string
}
type ListResponse[T Resource] struct {
Items []T
RV int64
Continue int64
}
type ListFunc[T Resource] func(ctx context.Context, ns claims.NamespaceInfo, p Pagination) (*ListResponse[T], error)
// List is a helper function that will perform access check on resources if
// prvovided with a claims.AccessClient.
func List[T Resource](
ctx context.Context,
resourceName string,
ac claims.AccessClient,
p Pagination,
fn ListFunc[T],
) (*ListResponse[T], error) {
ns, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, err
}
ident, err := identity.GetRequester(ctx)
if err != nil {
return nil, err
}
check := func(_ string, _ string) bool { return true }
if ac != nil {
var err error
check, err = ac.Compile(ctx, ident, claims.AccessRequest{
Verb: utils.VerbList,
Resource: resourceName,
Namespace: ns.Value,
})
if err != nil {
return nil, err
}
}
res := &ListResponse[T]{Items: make([]T, 0, p.Limit)}
first, err := fn(ctx, ns, p)
if err != nil {
return nil, err
}
for _, item := range first.Items {
if !check(ns.Value, item.AuthID()) {
continue
}
res.Items = append(res.Items, item)
}
res.Continue = first.Continue
res.RV = first.RV
outer:
for len(res.Items) < int(p.Limit) && res.Continue != 0 {
// FIXME: it is not optimal to reduce the amout we look for here but it is the easiest way to
// correctly handle pagination and continue tokens
r, err := fn(ctx, ns, Pagination{Limit: p.Limit - int64(len(res.Items)), Continue: res.Continue})
if err != nil {
return nil, err
}
for _, item := range r.Items {
if len(res.Items) == int(p.Limit) {
res.Continue = r.Continue
break outer
}
if !check(ns.Value, item.AuthID()) {
continue
}
res.Items = append(res.Items, item)
}
}
return res, nil
}
+105
View File
@@ -0,0 +1,105 @@
package common
import (
"context"
"testing"
"github.com/grafana/authlib/claims"
"github.com/stretchr/testify/assert"
"k8s.io/apiserver/pkg/endpoints/request"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
"github.com/grafana/grafana/pkg/services/featuremgmt"
)
type item struct {
id string
}
func (i item) AuthID() string {
return i.id
}
func TestList(t *testing.T) {
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient())
t.Run("should allow all items if no access client is passed", func(t *testing.T) {
ctx := newContext("stacks-1", newIdent())
res, err := List(ctx, "items", nil, Pagination{Limit: 2}, func(ctx context.Context, ns claims.NamespaceInfo, p Pagination) (*ListResponse[item], error) {
return &ListResponse[item]{
Items: []item{item{"1"}, item{"2"}},
}, nil
})
assert.NoError(t, err)
assert.Len(t, res.Items, 2)
})
t.Run("should filter out items that are allowed", func(t *testing.T) {
ctx := newContext("stacks-1", newIdent(accesscontrol.Permission{Action: "items:read", Scope: "items:uid:1"}))
a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{
Resource: "items",
Attr: "uid",
})
res, err := List(ctx, "items", a, Pagination{Limit: 2}, func(ctx context.Context, ns claims.NamespaceInfo, p Pagination) (*ListResponse[item], error) {
return &ListResponse[item]{
Items: []item{item{"1"}, item{"2"}},
}, nil
})
assert.NoError(t, err)
assert.Len(t, res.Items, 1)
})
t.Run("should fetch more for partial response with continue token", func(t *testing.T) {
ctx := newContext("stacks-1", newIdent(
accesscontrol.Permission{Action: "items:read", Scope: "items:uid:1"},
accesscontrol.Permission{Action: "items:read", Scope: "items:uid:3"},
))
a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{
Resource: "items",
Attr: "uid",
})
var called bool
res, err := List(ctx, "items", a, Pagination{Limit: 2}, func(ctx context.Context, ns claims.NamespaceInfo, p Pagination) (*ListResponse[item], error) {
if called {
return &ListResponse[item]{
Items: []item{item{"3"}},
}, nil
}
called = true
return &ListResponse[item]{
Items: []item{item{"1"}, item{"2"}},
Continue: 3,
}, nil
})
assert.NoError(t, err)
assert.Len(t, res.Items, 2)
assert.Equal(t, "1", res.Items[0].AuthID())
assert.Equal(t, "3", res.Items[1].AuthID())
})
}
func newContext(namespace string, ident *identity.StaticRequester) context.Context {
return request.WithNamespace(identity.WithRequester(context.Background(), ident), namespace)
}
func newIdent(permissions ...accesscontrol.Permission) *identity.StaticRequester {
pmap := map[string][]string{}
for _, p := range permissions {
pmap[p.Action] = append(pmap[p.Action], p.Scope)
}
return &identity.StaticRequester{
OrgID: 1,
Permissions: map[int64]map[string][]string{1: pmap},
}
}