grafana-iam: Implement api level user authorization (#114498)
* OnGoing comment * WIP on the wrapper * Get before Delete * WIP: add an unimplemented storage authorizer * WIP implementing the resource permission authorize * Implement beforeCreate * Create, Delete, Update * List * Use a resource permissions wrapper * Switch the main authorizer to service * Add namespace * Use compile for list * Comment * Remove unecessary comments * fix bug with folder permissions * Implement tests for List * Test get * List test small refactor * Delete test * Reorganize code * imports * Start splitting the tests * test AfterDelete * actually test beforeWrite * Implement tests for wrapper create * Test delete * Test List and Get * Fix List * Remaining tests * simplify * Remove comments * Reorder * Change authorizer to allow access
This commit is contained in:
@@ -22,6 +22,19 @@ type iamAuthorizer struct {
|
||||
func newIAMAuthorizer(accessClient authlib.AccessClient, legacyAccessClient authlib.AccessClient) authorizer.Authorizer {
|
||||
resourceAuthorizer := make(map[string]authorizer.Authorizer)
|
||||
|
||||
// Authorizer that allows any authenticated user
|
||||
// To be used when authorization is handled at the storage layer
|
||||
allowAuthorizer := authorizer.AuthorizerFunc(func(
|
||||
ctx context.Context, attr authorizer.Attributes,
|
||||
) (authorized authorizer.Decision, reason string, err error) {
|
||||
if !attr.IsResourceRequest() {
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
|
||||
// Any authenticated user can access the API
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
})
|
||||
|
||||
// Identity specific resources
|
||||
legacyAuthorizer := gfauthorizer.NewResourceAuthorizer(legacyAccessClient)
|
||||
resourceAuthorizer[iamv0.TeamBindingResourceInfo.GetName()] = legacyAuthorizer
|
||||
@@ -31,7 +44,7 @@ func newIAMAuthorizer(accessClient authlib.AccessClient, legacyAccessClient auth
|
||||
authorizer := gfauthorizer.NewResourceAuthorizer(accessClient)
|
||||
resourceAuthorizer[iamv0.CoreRoleInfo.GetName()] = iamauthorizer.NewCoreRoleAuthorizer(accessClient)
|
||||
resourceAuthorizer[iamv0.RoleInfo.GetName()] = authorizer
|
||||
resourceAuthorizer[iamv0.ResourcePermissionInfo.GetName()] = authorizer
|
||||
resourceAuthorizer[iamv0.ResourcePermissionInfo.GetName()] = allowAuthorizer // Handled at storage layer
|
||||
resourceAuthorizer[iamv0.RoleBindingInfo.GetName()] = authorizer
|
||||
resourceAuthorizer[iamv0.ServiceAccountResourceInfo.GetName()] = authorizer
|
||||
resourceAuthorizer[iamv0.UserResourceInfo.GetName()] = authorizer
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
package authorizer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/authlib/types"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer/storewrapper"
|
||||
)
|
||||
|
||||
// TODO: Logs, Metrics, Traces?
|
||||
|
||||
// ResourcePermissionsAuthorizer
|
||||
type ResourcePermissionsAuthorizer struct {
|
||||
accessClient types.AccessClient
|
||||
}
|
||||
|
||||
var _ storewrapper.ResourceStorageAuthorizer = (*ResourcePermissionsAuthorizer)(nil)
|
||||
|
||||
func NewResourcePermissionsAuthorizer(accessClient types.AccessClient) *ResourcePermissionsAuthorizer {
|
||||
return &ResourcePermissionsAuthorizer{
|
||||
accessClient: accessClient,
|
||||
}
|
||||
}
|
||||
|
||||
// AfterGet implements ResourceStorageAuthorizer.
|
||||
func (r *ResourcePermissionsAuthorizer) AfterGet(ctx context.Context, obj runtime.Object) error {
|
||||
authInfo, ok := types.AuthInfoFrom(ctx)
|
||||
if !ok {
|
||||
return storewrapper.ErrUnauthenticated
|
||||
}
|
||||
switch o := obj.(type) {
|
||||
case *iamv0.ResourcePermission:
|
||||
target := o.Spec.Resource
|
||||
|
||||
// TODO: Fetch the resource to retrieve its parent folder.
|
||||
parent := ""
|
||||
|
||||
checkReq := types.CheckRequest{
|
||||
Namespace: o.Namespace,
|
||||
Group: target.ApiGroup,
|
||||
Resource: target.Resource,
|
||||
Verb: utils.VerbGetPermissions,
|
||||
Name: target.Name,
|
||||
}
|
||||
res, err := r.accessClient.Check(ctx, authInfo, checkReq, parent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !res.Allowed {
|
||||
return storewrapper.ErrUnauthorized
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("expected ResourcePermission, got %T: %w", o, storewrapper.ErrUnexpectedType)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ResourcePermissionsAuthorizer) beforeWrite(ctx context.Context, obj runtime.Object) error {
|
||||
authInfo, ok := types.AuthInfoFrom(ctx)
|
||||
if !ok {
|
||||
return storewrapper.ErrUnauthenticated
|
||||
}
|
||||
switch o := obj.(type) {
|
||||
case *iamv0.ResourcePermission:
|
||||
target := o.Spec.Resource
|
||||
|
||||
// TODO: Fetch the resource to retrieve its parent folder.
|
||||
parent := ""
|
||||
|
||||
checkReq := types.CheckRequest{
|
||||
Namespace: o.Namespace,
|
||||
Group: target.ApiGroup,
|
||||
Resource: target.Resource,
|
||||
Verb: utils.VerbSetPermissions,
|
||||
Name: target.Name,
|
||||
}
|
||||
res, err := r.accessClient.Check(ctx, authInfo, checkReq, parent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !res.Allowed {
|
||||
return storewrapper.ErrUnauthorized
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("expected ResourcePermission, got %T: %w", o, storewrapper.ErrUnexpectedType)
|
||||
}
|
||||
}
|
||||
|
||||
// BeforeCreate implements ResourceStorageAuthorizer.
|
||||
func (r *ResourcePermissionsAuthorizer) BeforeCreate(ctx context.Context, obj runtime.Object) error {
|
||||
return r.beforeWrite(ctx, obj)
|
||||
}
|
||||
|
||||
// BeforeDelete implements ResourceStorageAuthorizer.
|
||||
func (r *ResourcePermissionsAuthorizer) BeforeDelete(ctx context.Context, obj runtime.Object) error {
|
||||
return r.beforeWrite(ctx, obj)
|
||||
}
|
||||
|
||||
// BeforeUpdate implements ResourceStorageAuthorizer.
|
||||
func (r *ResourcePermissionsAuthorizer) BeforeUpdate(ctx context.Context, obj runtime.Object) error {
|
||||
return r.beforeWrite(ctx, obj)
|
||||
}
|
||||
|
||||
// FilterList implements ResourceStorageAuthorizer.
|
||||
func (r *ResourcePermissionsAuthorizer) FilterList(ctx context.Context, list runtime.Object) (runtime.Object, error) {
|
||||
authInfo, ok := types.AuthInfoFrom(ctx)
|
||||
if !ok {
|
||||
return nil, storewrapper.ErrUnauthenticated
|
||||
}
|
||||
|
||||
switch l := list.(type) {
|
||||
case *iamv0.ResourcePermissionList:
|
||||
var (
|
||||
filteredItems []iamv0.ResourcePermission
|
||||
err error
|
||||
canViewFuncs = map[schema.GroupResource]types.ItemChecker{}
|
||||
)
|
||||
for _, item := range l.Items {
|
||||
gr := schema.GroupResource{
|
||||
Group: item.Spec.Resource.ApiGroup,
|
||||
Resource: item.Spec.Resource.Resource,
|
||||
}
|
||||
|
||||
// Reuse the same canView for items with the same resource
|
||||
canView, found := canViewFuncs[gr]
|
||||
|
||||
if !found {
|
||||
listReq := types.ListRequest{
|
||||
Namespace: item.Namespace,
|
||||
Group: item.Spec.Resource.ApiGroup,
|
||||
Resource: item.Spec.Resource.Resource,
|
||||
Verb: utils.VerbGetPermissions,
|
||||
}
|
||||
|
||||
canView, _, err = r.accessClient.Compile(ctx, authInfo, listReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
canViewFuncs[gr] = canView
|
||||
}
|
||||
|
||||
// TODO : Fetch the resource to retrieve its parent folder.
|
||||
parent := ""
|
||||
|
||||
allowed := canView(item.Spec.Resource.Name, parent)
|
||||
if allowed {
|
||||
filteredItems = append(filteredItems, item)
|
||||
}
|
||||
}
|
||||
l.Items = filteredItems
|
||||
return l, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("expected ResourcePermissionList, got %T: %w", l, storewrapper.ErrUnexpectedType)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
package authorizer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/go-jose/go-jose/v4/jwt"
|
||||
"github.com/grafana/authlib/authn"
|
||||
"github.com/grafana/authlib/types"
|
||||
iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
var (
|
||||
user = authn.NewIDTokenAuthInfo(
|
||||
authn.Claims[authn.AccessTokenClaims]{
|
||||
Claims: jwt.Claims{Issuer: "grafana",
|
||||
Subject: types.NewTypeID(types.TypeAccessPolicy, "grafana"), Audience: []string{"iam.grafana.app"}},
|
||||
Rest: authn.AccessTokenClaims{
|
||||
Namespace: "*",
|
||||
Permissions: identity.ServiceIdentityClaims.Rest.Permissions,
|
||||
DelegatedPermissions: identity.ServiceIdentityClaims.Rest.DelegatedPermissions,
|
||||
},
|
||||
}, &authn.Claims[authn.IDTokenClaims]{
|
||||
Claims: jwt.Claims{Subject: types.NewTypeID(types.TypeUser, "u001")},
|
||||
Rest: authn.IDTokenClaims{Namespace: "org-2", Identifier: "u001", Type: types.TypeUser},
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
func newResourcePermission(apiGroup, resource, name string) *iamv0.ResourcePermission {
|
||||
return &iamv0.ResourcePermission{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: "org-2"},
|
||||
Spec: iamv0.ResourcePermissionSpec{
|
||||
Resource: iamv0.ResourcePermissionspecResource{
|
||||
ApiGroup: apiGroup,
|
||||
Resource: resource,
|
||||
Name: name,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourcePermissions_AfterGet(t *testing.T) {
|
||||
// In this test, we verify that AfterGet calls accessClient.Check with the correct parameters
|
||||
fold1 := newResourcePermission("folder.grafana.app", "folders", "fold-1")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
shouldAllow bool
|
||||
}{
|
||||
{
|
||||
name: "allow access",
|
||||
shouldAllow: true,
|
||||
},
|
||||
{
|
||||
name: "deny access",
|
||||
shouldAllow: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
checkFunc := func(id types.AuthInfo, req *types.CheckRequest, folder string) (types.CheckResponse, error) {
|
||||
require.NotNil(t, id)
|
||||
// Check is called with the user's identity
|
||||
require.Equal(t, "user:u001", id.GetUID())
|
||||
require.Equal(t, "org-2", id.GetNamespace())
|
||||
// Check the request values
|
||||
require.Equal(t, "org-2", req.Namespace)
|
||||
require.Equal(t, fold1.Spec.Resource.ApiGroup, req.Group)
|
||||
require.Equal(t, fold1.Spec.Resource.Resource, req.Resource)
|
||||
require.Equal(t, fold1.Spec.Resource.Name, req.Name)
|
||||
require.Equal(t, utils.VerbGetPermissions, req.Verb)
|
||||
|
||||
return types.CheckResponse{Allowed: tt.shouldAllow}, nil
|
||||
}
|
||||
|
||||
accessClient := &fakeAccessClient{checkFunc: checkFunc}
|
||||
resPermAuthz := NewResourcePermissionsAuthorizer(accessClient)
|
||||
ctx := types.WithAuthInfo(context.Background(), user)
|
||||
|
||||
err := resPermAuthz.AfterGet(ctx, fold1)
|
||||
if tt.shouldAllow {
|
||||
require.NoError(t, err, "expected no error for allowed access")
|
||||
} else {
|
||||
require.Error(t, err, "expected error for denied access")
|
||||
}
|
||||
require.True(t, accessClient.checkCalled, "accessClient.Check should be called")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourcePermissions_FilterList(t *testing.T) {
|
||||
// In this test, the user has permission to access only fold-1 and dash-2.
|
||||
// We verify that FilterList returns only those two objects.
|
||||
|
||||
list := &iamv0.ResourcePermissionList{
|
||||
Items: []iamv0.ResourcePermission{
|
||||
*newResourcePermission("folder.grafana.app", "folders", "fold-1"),
|
||||
*newResourcePermission("folder.grafana.app", "folders", "fold-2"),
|
||||
*newResourcePermission("dashboard.grafana.app", "dashboards", "dash-2"),
|
||||
},
|
||||
}
|
||||
|
||||
compileFunc := func(id types.AuthInfo, req types.ListRequest) (types.ItemChecker, types.Zookie, error) {
|
||||
require.NotNil(t, id)
|
||||
// Compile is called with the user's identity
|
||||
require.Equal(t, "user:u001", id.GetUID())
|
||||
require.Equal(t, "org-2", id.GetNamespace())
|
||||
// Check the request values
|
||||
require.Equal(t, "org-2", req.Namespace)
|
||||
if req.Resource == "folders" {
|
||||
require.Equal(t, "folder.grafana.app", req.Group)
|
||||
require.Equal(t, "folders", req.Resource)
|
||||
}
|
||||
if req.Resource == "dashboards" {
|
||||
require.Equal(t, "dashboard.grafana.app", req.Group)
|
||||
require.Equal(t, "dashboards", req.Resource)
|
||||
}
|
||||
|
||||
// Return a checker that allows only specific resources: fold-1 and dash-2
|
||||
return func(name, folder string) bool {
|
||||
if name == "fold-1" || name == "dash-2" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}, &types.NoopZookie{}, nil
|
||||
}
|
||||
|
||||
accessClient := &fakeAccessClient{compileFunc: compileFunc}
|
||||
resPermAuthz := NewResourcePermissionsAuthorizer(accessClient)
|
||||
ctx := types.WithAuthInfo(context.Background(), user)
|
||||
|
||||
obj, err := resPermAuthz.FilterList(ctx, list)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, list)
|
||||
require.True(t, accessClient.compileCalled, "accessClient.Compile should be called")
|
||||
|
||||
filtered, ok := obj.(*iamv0.ResourcePermissionList)
|
||||
require.True(t, ok, "response should be of type ResourcePermissionList")
|
||||
require.Len(t, filtered.Items, 2, "response list should have 2 items after filtering")
|
||||
require.Equal(t, "fold-1", filtered.Items[0].Spec.Resource.Name)
|
||||
require.Equal(t, "dash-2", filtered.Items[1].Spec.Resource.Name)
|
||||
}
|
||||
|
||||
func TestResourcePermissions_beforeWrite(t *testing.T) {
|
||||
// In this test, we verify that beforeWrite calls accessClient.Check with the correct parameters
|
||||
fold1 := newResourcePermission("folder.grafana.app", "folders", "fold-1")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
shouldAllow bool
|
||||
}{
|
||||
{
|
||||
name: "allow delete",
|
||||
shouldAllow: true,
|
||||
},
|
||||
{
|
||||
name: "deny delete",
|
||||
shouldAllow: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
checkFunc := func(id types.AuthInfo, req *types.CheckRequest, folder string) (types.CheckResponse, error) {
|
||||
require.NotNil(t, id)
|
||||
// Check is called with the user's identity
|
||||
require.Equal(t, "user:u001", id.GetUID())
|
||||
require.Equal(t, "org-2", id.GetNamespace())
|
||||
// Check the request values
|
||||
require.Equal(t, "org-2", req.Namespace)
|
||||
require.Equal(t, fold1.Spec.Resource.ApiGroup, req.Group)
|
||||
require.Equal(t, fold1.Spec.Resource.Resource, req.Resource)
|
||||
require.Equal(t, fold1.Spec.Resource.Name, req.Name)
|
||||
require.Equal(t, utils.VerbSetPermissions, req.Verb)
|
||||
|
||||
return types.CheckResponse{Allowed: tt.shouldAllow}, nil
|
||||
}
|
||||
|
||||
accessClient := &fakeAccessClient{checkFunc: checkFunc}
|
||||
resPermAuthz := NewResourcePermissionsAuthorizer(accessClient)
|
||||
ctx := types.WithAuthInfo(context.Background(), user)
|
||||
|
||||
err := resPermAuthz.beforeWrite(ctx, fold1)
|
||||
if tt.shouldAllow {
|
||||
require.NoError(t, err, "expected no error for allowed delete")
|
||||
} else {
|
||||
require.Error(t, err, "expected error for denied delete")
|
||||
}
|
||||
require.True(t, accessClient.checkCalled, "accessClient.Check should be called")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// fakeAccessClient is a mock implementation of claims.AccessClient
|
||||
type fakeAccessClient struct {
|
||||
checkCalled bool
|
||||
checkFunc func(id types.AuthInfo, req *types.CheckRequest, folder string) (types.CheckResponse, error)
|
||||
compileCalled bool
|
||||
compileFunc func(id types.AuthInfo, req types.ListRequest) (types.ItemChecker, types.Zookie, error)
|
||||
}
|
||||
|
||||
func (m *fakeAccessClient) Check(ctx context.Context, id types.AuthInfo, req types.CheckRequest, folder string) (types.CheckResponse, error) {
|
||||
m.checkCalled = true
|
||||
return m.checkFunc(id, &req, folder)
|
||||
}
|
||||
|
||||
func (m *fakeAccessClient) Compile(ctx context.Context, id types.AuthInfo, req types.ListRequest) (types.ItemChecker, types.Zookie, error) {
|
||||
m.compileCalled = true
|
||||
return m.compileFunc(id, req)
|
||||
}
|
||||
|
||||
var _ types.AccessClient = (*fakeAccessClient)(nil)
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
iamauthorizer "github.com/grafana/grafana/pkg/registry/apis/iam/authorizer"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/iam/externalgroupmapping"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/iam/resourcepermission"
|
||||
@@ -39,6 +40,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/registry/apis/iam/user"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
gfauthorizer "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer/storewrapper"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
@@ -402,7 +404,15 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateResourcePermissionsAPIGroup(
|
||||
return err
|
||||
}
|
||||
|
||||
storage[iamv0.ResourcePermissionInfo.StoragePath()] = dw
|
||||
// Not ideal, the alternative is to wrap both stores that dualwrite uses
|
||||
regStoreDW, ok := dw.(*registry.Store)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected RegistryStoreDualWrite, got %T", dw)
|
||||
}
|
||||
|
||||
authzWrapper := storewrapper.New(regStoreDW, iamauthorizer.NewResourcePermissionsAuthorizer(b.accessClient))
|
||||
|
||||
storage[iamv0.ResourcePermissionInfo.StoragePath()] = authzWrapper
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
package storewrapper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
k8srest "k8s.io/apiserver/pkg/registry/rest"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnauthenticated = fmt.Errorf("unauthenticated")
|
||||
ErrUnauthorized = fmt.Errorf("unauthorized")
|
||||
ErrUnexpectedType = fmt.Errorf("unexpected object type")
|
||||
)
|
||||
|
||||
// ResourceStorageAuthorizer defines authorization hooks for resource storage operations.
|
||||
type ResourceStorageAuthorizer interface {
|
||||
BeforeCreate(ctx context.Context, obj runtime.Object) error
|
||||
BeforeUpdate(ctx context.Context, obj runtime.Object) error
|
||||
BeforeDelete(ctx context.Context, obj runtime.Object) error
|
||||
AfterGet(ctx context.Context, obj runtime.Object) error
|
||||
FilterList(ctx context.Context, list runtime.Object) (runtime.Object, error)
|
||||
}
|
||||
|
||||
// Wrapper is a k8sStorage (e.g. registry.Store) wrapper that enforces authorization based on ResourceStorageAuthorizer.
|
||||
// It overrides the identity in the context to use service identity for the underlying store operations.
|
||||
// That way, the underlying store authorization is always successful, and the authorization is enforced by the wrapper.
|
||||
type Wrapper struct {
|
||||
inner K8sStorage
|
||||
authorizer ResourceStorageAuthorizer
|
||||
}
|
||||
|
||||
type K8sStorage interface {
|
||||
k8srest.Storage
|
||||
k8srest.Scoper
|
||||
k8srest.SingularNameProvider
|
||||
k8srest.Lister
|
||||
k8srest.Getter
|
||||
k8srest.CreaterUpdater
|
||||
k8srest.GracefulDeleter
|
||||
}
|
||||
|
||||
var _ rest.Storage = (*Wrapper)(nil)
|
||||
|
||||
func New(store K8sStorage, authz ResourceStorageAuthorizer) *Wrapper {
|
||||
return &Wrapper{inner: store, authorizer: authz}
|
||||
}
|
||||
|
||||
func (w *Wrapper) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metaV1.Table, error) {
|
||||
return w.inner.ConvertToTable(ctx, object, tableOptions)
|
||||
}
|
||||
|
||||
func (w *Wrapper) Create(ctx context.Context, obj runtime.Object, createValidation k8srest.ValidateObjectFunc, options *metaV1.CreateOptions) (runtime.Object, error) {
|
||||
// Enforce authorization based on the user permissions before creating the object
|
||||
err := w.authorizer.BeforeCreate(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Override the identity to use service identity for the underlying store operation
|
||||
srvCtx, _ := identity.WithServiceIdentity(ctx, 0)
|
||||
|
||||
return w.inner.Create(srvCtx, obj, createValidation, options)
|
||||
}
|
||||
|
||||
func (w *Wrapper) Delete(ctx context.Context, name string, deleteValidation k8srest.ValidateObjectFunc, options *metaV1.DeleteOptions) (runtime.Object, bool, error) {
|
||||
// Fetch the object first to authorize
|
||||
srvCtx, _ := identity.WithServiceIdentity(ctx, 0)
|
||||
getOpts := &metaV1.GetOptions{TypeMeta: options.TypeMeta}
|
||||
if options.Preconditions != nil {
|
||||
getOpts.ResourceVersion = *options.Preconditions.ResourceVersion
|
||||
}
|
||||
obj, err := w.inner.Get(srvCtx, name, getOpts)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
// Enforce authorization based on the user permissions
|
||||
if err := w.authorizer.BeforeDelete(ctx, obj); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return w.inner.Delete(srvCtx, name, deleteValidation, options)
|
||||
}
|
||||
|
||||
func (w *Wrapper) DeleteCollection(ctx context.Context, deleteValidation k8srest.ValidateObjectFunc, options *metaV1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) {
|
||||
// DeleteCollection is complex to authorize properly
|
||||
// For now, deny it entirely for safety
|
||||
return nil, fmt.Errorf("bulk delete operations are not supported through this API")
|
||||
}
|
||||
|
||||
func (w *Wrapper) Destroy() {
|
||||
w.inner.Destroy()
|
||||
}
|
||||
|
||||
func (w *Wrapper) Get(ctx context.Context, name string, options *metaV1.GetOptions) (runtime.Object, error) {
|
||||
// Override the identity to use service identity for the underlying store operation
|
||||
srvCtx, _ := identity.WithServiceIdentity(ctx, 0)
|
||||
|
||||
item, err := w.inner.Get(srvCtx, name, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Enforce authorization based on the user permissions after retrieving the object
|
||||
err = w.authorizer.AfterGet(ctx, item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (w *Wrapper) GetSingularName() string {
|
||||
return w.inner.GetSingularName()
|
||||
}
|
||||
|
||||
func (w *Wrapper) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
|
||||
// Override the identity to use service identity for the underlying store operation
|
||||
srvCtx, _ := identity.WithServiceIdentity(ctx, 0)
|
||||
|
||||
list, err := w.inner.List(srvCtx, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Enforce authorization based on the user permissions after retrieving the list
|
||||
return w.authorizer.FilterList(ctx, list)
|
||||
}
|
||||
|
||||
func (w *Wrapper) NamespaceScoped() bool {
|
||||
return w.inner.NamespaceScoped()
|
||||
}
|
||||
|
||||
func (w *Wrapper) New() runtime.Object {
|
||||
return w.inner.New()
|
||||
}
|
||||
|
||||
func (w *Wrapper) NewList() runtime.Object {
|
||||
return w.inner.NewList()
|
||||
}
|
||||
|
||||
func (w *Wrapper) Update(
|
||||
ctx context.Context,
|
||||
name string,
|
||||
objInfo k8srest.UpdatedObjectInfo,
|
||||
createValidation k8srest.ValidateObjectFunc,
|
||||
updateValidation k8srest.ValidateObjectUpdateFunc,
|
||||
forceAllowCreate bool,
|
||||
options *metaV1.UpdateOptions,
|
||||
) (runtime.Object, bool, error) {
|
||||
// Create a wrapper around UpdatedObjectInfo to inject authorization
|
||||
wrappedObjInfo := &authorizedUpdateInfo{
|
||||
inner: objInfo,
|
||||
authorizer: w.authorizer,
|
||||
userCtx: ctx, // Keep original context for authorization
|
||||
}
|
||||
|
||||
// Override the identity to use service identity for the underlying store operation
|
||||
srvCtx, _ := identity.WithServiceIdentity(ctx, 0)
|
||||
|
||||
return w.inner.Update(srvCtx, name, wrappedObjInfo, createValidation, updateValidation, forceAllowCreate, options)
|
||||
}
|
||||
|
||||
type authorizedUpdateInfo struct {
|
||||
inner k8srest.UpdatedObjectInfo
|
||||
authorizer ResourceStorageAuthorizer
|
||||
userCtx context.Context
|
||||
}
|
||||
|
||||
func (a *authorizedUpdateInfo) Preconditions() *metaV1.Preconditions {
|
||||
return a.inner.Preconditions()
|
||||
}
|
||||
|
||||
func (a *authorizedUpdateInfo) UpdatedObject(ctx context.Context, oldObj runtime.Object) (runtime.Object, error) {
|
||||
// Get the updated object
|
||||
updatedObj, err := a.inner.UpdatedObject(ctx, oldObj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Enforce authorization using the original user context
|
||||
if err := a.authorizer.BeforeUpdate(a.userCtx, updatedObj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return updatedObj, nil
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
package storewrapper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/authlib/types"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
type testSetup struct {
|
||||
mockStore *rest.MockStorage
|
||||
mockAuth *FakeAuthorizer
|
||||
wrapper *Wrapper
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func newTestSetup(t *testing.T) *testSetup {
|
||||
mockStore := rest.NewMockStorage(t)
|
||||
mockAuth := &FakeAuthorizer{}
|
||||
wrapper := New(mockStore, mockAuth)
|
||||
|
||||
ctx := identity.WithRequester(
|
||||
context.Background(),
|
||||
&identity.StaticRequester{UserUID: "u001", Type: types.TypeUser},
|
||||
)
|
||||
|
||||
return &testSetup{mockStore: mockStore, mockAuth: mockAuth, wrapper: wrapper, ctx: ctx}
|
||||
}
|
||||
|
||||
func matchesOriginalUser() func(context.Context) bool {
|
||||
return func(ctx context.Context) bool {
|
||||
user, err := identity.GetRequester(ctx)
|
||||
return err == nil && user.GetUID() == "user:u001"
|
||||
}
|
||||
}
|
||||
|
||||
func matchesServiceIdentity() func(context.Context) bool {
|
||||
return func(ctx context.Context) bool {
|
||||
return identity.IsServiceIdentity(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapper_Create(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
setup := newTestSetup(t)
|
||||
|
||||
obj := &fakeObject{}
|
||||
createOpts := &metaV1.CreateOptions{}
|
||||
expectedObj := &fakeObject{ObjectMeta: metaV1.ObjectMeta{Name: "created"}}
|
||||
|
||||
// Verify original user identity is used for authorization
|
||||
setup.mockAuth.On("BeforeCreate", mock.MatchedBy(matchesOriginalUser()), obj).Return(nil)
|
||||
|
||||
// Verify service identity is used to call the underlying store
|
||||
setup.mockStore.On("Create", mock.MatchedBy(matchesServiceIdentity()), obj, mock.Anything, createOpts).Return(expectedObj, nil)
|
||||
|
||||
result, err := setup.wrapper.Create(setup.ctx, obj, nil, createOpts)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedObj, result)
|
||||
|
||||
// Assert expectations
|
||||
setup.mockAuth.AssertExpectations(t)
|
||||
setup.mockStore.AssertExpectations(t)
|
||||
})
|
||||
t.Run("unauthorized", func(t *testing.T) {
|
||||
setup := newTestSetup(t)
|
||||
|
||||
obj := &fakeObject{}
|
||||
createOpts := &metaV1.CreateOptions{}
|
||||
|
||||
// Simulate unauthorized error from authorizer
|
||||
setup.mockAuth.On("BeforeCreate", mock.MatchedBy(matchesOriginalUser()), obj).Return(ErrUnauthorized)
|
||||
|
||||
result, err := setup.wrapper.Create(setup.ctx, obj, nil, createOpts)
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
assert.Equal(t, ErrUnauthorized, err)
|
||||
|
||||
// Assert expectations
|
||||
setup.mockAuth.AssertExpectations(t)
|
||||
setup.mockStore.AssertNotCalled(t, "Create")
|
||||
})
|
||||
}
|
||||
|
||||
func TestWrapper_Delete(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
setup := newTestSetup(t)
|
||||
version := "1"
|
||||
obj := &fakeObject{ObjectMeta: metaV1.ObjectMeta{Name: "to-delete"}}
|
||||
deleteOpts := &metaV1.DeleteOptions{Preconditions: &metaV1.Preconditions{ResourceVersion: &version}}
|
||||
expectedObj := &fakeObject{ObjectMeta: metaV1.ObjectMeta{Name: "deleted"}}
|
||||
|
||||
// Mock Get to fetch the object before deletion
|
||||
setup.mockStore.On("Get", mock.MatchedBy(matchesServiceIdentity()), "to-delete", mock.Anything).Return(obj, nil)
|
||||
|
||||
// Verify original user identity is used for authorization
|
||||
setup.mockAuth.On("BeforeDelete", mock.MatchedBy(matchesOriginalUser()), obj).Return(nil)
|
||||
|
||||
// Verify service identity is used to call the underlying store
|
||||
setup.mockStore.On("Delete", mock.MatchedBy(matchesServiceIdentity()), "to-delete", mock.Anything, deleteOpts).Return(expectedObj, true, nil)
|
||||
|
||||
result, deleted, err := setup.wrapper.Delete(setup.ctx, "to-delete", nil, deleteOpts)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedObj, result)
|
||||
assert.True(t, deleted)
|
||||
|
||||
// Assert expectations
|
||||
setup.mockAuth.AssertExpectations(t)
|
||||
setup.mockStore.AssertExpectations(t)
|
||||
})
|
||||
t.Run("unauthorized", func(t *testing.T) {
|
||||
setup := newTestSetup(t)
|
||||
version := "1"
|
||||
obj := &fakeObject{ObjectMeta: metaV1.ObjectMeta{Name: "to-delete"}}
|
||||
deleteOpts := &metaV1.DeleteOptions{Preconditions: &metaV1.Preconditions{ResourceVersion: &version}}
|
||||
|
||||
// Mock Get to fetch the object before deletion
|
||||
setup.mockStore.On("Get", mock.MatchedBy(matchesServiceIdentity()), "to-delete", mock.Anything).Return(obj, nil)
|
||||
|
||||
// Simulate unauthorized error from authorizer
|
||||
setup.mockAuth.On("BeforeDelete", mock.MatchedBy(matchesOriginalUser()), obj).Return(ErrUnauthorized)
|
||||
|
||||
result, deleted, err := setup.wrapper.Delete(setup.ctx, "to-delete", nil, deleteOpts)
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
assert.False(t, deleted)
|
||||
assert.Equal(t, ErrUnauthorized, err)
|
||||
|
||||
// Assert expectations
|
||||
setup.mockAuth.AssertExpectations(t)
|
||||
setup.mockStore.AssertExpectations(t)
|
||||
setup.mockStore.AssertNotCalled(t, "Delete")
|
||||
})
|
||||
}
|
||||
|
||||
func TestWrapper_Get(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
setup := newTestSetup(t)
|
||||
|
||||
obj := &fakeObject{ObjectMeta: metaV1.ObjectMeta{Name: "fetched"}}
|
||||
|
||||
// Verify service identity is used to call the underlying store
|
||||
setup.mockStore.On("Get", mock.MatchedBy(matchesServiceIdentity()), "fetched", mock.Anything).Return(obj, nil)
|
||||
|
||||
// Verify original user identity is used for after-get authorization
|
||||
setup.mockAuth.On("AfterGet", mock.MatchedBy(matchesOriginalUser()), obj).Return(nil)
|
||||
|
||||
result, err := setup.wrapper.Get(setup.ctx, "fetched", &metaV1.GetOptions{})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, obj, result)
|
||||
|
||||
// Assert expectations
|
||||
setup.mockAuth.AssertExpectations(t)
|
||||
setup.mockStore.AssertExpectations(t)
|
||||
})
|
||||
t.Run("unauthorized", func(t *testing.T) {
|
||||
setup := newTestSetup(t)
|
||||
|
||||
obj := &fakeObject{ObjectMeta: metaV1.ObjectMeta{Name: "fetched"}}
|
||||
|
||||
// Verify service identity is used to call the underlying store
|
||||
setup.mockStore.On("Get", mock.MatchedBy(matchesServiceIdentity()), "fetched", mock.Anything).Return(obj, nil)
|
||||
|
||||
// Simulate unauthorized error from after-get authorizer
|
||||
setup.mockAuth.On("AfterGet", mock.MatchedBy(matchesOriginalUser()), obj).Return(ErrUnauthorized)
|
||||
|
||||
result, err := setup.wrapper.Get(setup.ctx, "fetched", &metaV1.GetOptions{})
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
assert.Equal(t, ErrUnauthorized, err)
|
||||
|
||||
// Assert expectations
|
||||
setup.mockAuth.AssertExpectations(t)
|
||||
setup.mockStore.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWrapper_List(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
setup := newTestSetup(t)
|
||||
|
||||
listObj := &metaV1.List{Items: []runtime.RawExtension{
|
||||
{Object: &fakeObject{ObjectMeta: metaV1.ObjectMeta{Name: "item1"}}},
|
||||
{Object: &fakeObject{ObjectMeta: metaV1.ObjectMeta{Name: "item2"}}},
|
||||
}}
|
||||
|
||||
filteredListObj := &metaV1.List{Items: []runtime.RawExtension{
|
||||
{Object: &fakeObject{ObjectMeta: metaV1.ObjectMeta{Name: "item1"}}},
|
||||
}}
|
||||
|
||||
// Verify service identity is used to call the underlying store
|
||||
setup.mockStore.On("List", mock.MatchedBy(matchesServiceIdentity()), mock.Anything).Return(listObj, nil)
|
||||
|
||||
// Verify original user identity is used for filtering the list
|
||||
setup.mockAuth.On("FilterList", mock.MatchedBy(matchesOriginalUser()), listObj).Return(filteredListObj, nil)
|
||||
|
||||
result, err := setup.wrapper.List(setup.ctx, &internalversion.ListOptions{})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, filteredListObj, result)
|
||||
|
||||
// Assert expectations
|
||||
setup.mockAuth.AssertExpectations(t)
|
||||
setup.mockStore.AssertExpectations(t)
|
||||
})
|
||||
t.Run("unauthorized", func(t *testing.T) {
|
||||
setup := newTestSetup(t)
|
||||
|
||||
listObj := &metaV1.List{Items: []runtime.RawExtension{
|
||||
{Object: &fakeObject{ObjectMeta: metaV1.ObjectMeta{Name: "item1"}}},
|
||||
{Object: &fakeObject{ObjectMeta: metaV1.ObjectMeta{Name: "item2"}}},
|
||||
}}
|
||||
|
||||
// Verify service identity is used to call the underlying store
|
||||
setup.mockStore.On("List", mock.MatchedBy(matchesServiceIdentity()), mock.Anything).Return(listObj, nil)
|
||||
|
||||
// Simulate unauthorized error from FilterList authorizer
|
||||
setup.mockAuth.On("FilterList", mock.MatchedBy(matchesOriginalUser()), listObj).Return(nil, ErrUnauthorized)
|
||||
|
||||
result, err := setup.wrapper.List(setup.ctx, &internalversion.ListOptions{})
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
assert.Equal(t, ErrUnauthorized, err)
|
||||
|
||||
// Assert expectations
|
||||
setup.mockAuth.AssertExpectations(t)
|
||||
setup.mockStore.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWrapper_Update(t *testing.T) {
|
||||
setup := newTestSetup(t)
|
||||
|
||||
oldObj := &fakeObject{ObjectMeta: metaV1.ObjectMeta{
|
||||
Name: "to-update", ResourceVersion: "2", Labels: map[string]string{"updated": "false"},
|
||||
}}
|
||||
objInfo := &fakeUpdatedObjectInfo{obj: oldObj}
|
||||
updateOpts := &metaV1.UpdateOptions{}
|
||||
|
||||
var authzInfo *authorizedUpdateInfo
|
||||
|
||||
// Verify service identity is used to call the underlying store
|
||||
setup.mockStore.On("Update",
|
||||
mock.MatchedBy(matchesServiceIdentity()),
|
||||
"to-update",
|
||||
mock.MatchedBy(func(info *authorizedUpdateInfo) bool {
|
||||
// Capture the authorizedUpdateInfo for later verification
|
||||
authzInfo = info
|
||||
return true
|
||||
}),
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
false,
|
||||
updateOpts).Return(oldObj, true, nil)
|
||||
|
||||
result, updated, err := setup.wrapper.Update(setup.ctx, "to-update", objInfo, nil, nil, false, updateOpts)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, oldObj, result)
|
||||
assert.True(t, updated)
|
||||
|
||||
// Now verify that the authorization is performed inside UpdatedObject
|
||||
setup.mockAuth.On("BeforeUpdate", mock.MatchedBy(matchesOriginalUser()), oldObj).Return(nil)
|
||||
obj, err := authzInfo.UpdatedObject(context.Background(), oldObj)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, oldObj, obj)
|
||||
|
||||
// Assert expectations
|
||||
setup.mockAuth.AssertExpectations(t)
|
||||
setup.mockStore.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestWrapper_DeleteCollection(t *testing.T) {
|
||||
setup := newTestSetup(t)
|
||||
|
||||
result, err := setup.wrapper.DeleteCollection(setup.ctx, nil, &metaV1.DeleteOptions{}, &internalversion.ListOptions{})
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "bulk delete operations are not supported")
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
|
||||
func TestWrapper_PassthroughMethods(t *testing.T) {
|
||||
setup := newTestSetup(t)
|
||||
|
||||
t.Run("New", func(t *testing.T) {
|
||||
obj := &fakeObject{}
|
||||
setup.mockStore.On("New").Return(obj).Once()
|
||||
assert.Equal(t, obj, setup.wrapper.New())
|
||||
})
|
||||
|
||||
t.Run("NewList", func(t *testing.T) {
|
||||
obj := &fakeObject{}
|
||||
setup.mockStore.On("NewList").Return(obj).Once()
|
||||
assert.Equal(t, obj, setup.wrapper.NewList())
|
||||
})
|
||||
|
||||
t.Run("GetSingularName", func(t *testing.T) {
|
||||
setup.mockStore.On("GetSingularName").Return("fake").Once()
|
||||
assert.Equal(t, "fake", setup.wrapper.GetSingularName())
|
||||
})
|
||||
|
||||
t.Run("NamespaceScoped", func(t *testing.T) {
|
||||
setup.mockStore.On("NamespaceScoped").Return(true).Once()
|
||||
assert.True(t, setup.wrapper.NamespaceScoped())
|
||||
})
|
||||
|
||||
t.Run("Destroy", func(t *testing.T) {
|
||||
setup.mockStore.On("Destroy").Once()
|
||||
setup.wrapper.Destroy()
|
||||
})
|
||||
|
||||
t.Run("ConvertToTable", func(t *testing.T) {
|
||||
obj := &fakeObject{}
|
||||
table := &metaV1.Table{}
|
||||
setup.mockStore.On("ConvertToTable", setup.ctx, obj, mock.Anything).Return(table, nil).Once()
|
||||
result, err := setup.wrapper.ConvertToTable(setup.ctx, obj, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, table, result)
|
||||
})
|
||||
|
||||
setup.mockStore.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// -----
|
||||
// Fakes
|
||||
// -----
|
||||
|
||||
type FakeAuthorizer struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (f *FakeAuthorizer) BeforeCreate(ctx context.Context, obj runtime.Object) error {
|
||||
args := f.Called(ctx, obj)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (f *FakeAuthorizer) BeforeUpdate(ctx context.Context, obj runtime.Object) error {
|
||||
args := f.Called(ctx, obj)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (f *FakeAuthorizer) BeforeDelete(ctx context.Context, obj runtime.Object) error {
|
||||
args := f.Called(ctx, obj)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (f *FakeAuthorizer) AfterGet(ctx context.Context, obj runtime.Object) error {
|
||||
args := f.Called(ctx, obj)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (f *FakeAuthorizer) FilterList(ctx context.Context, list runtime.Object) (runtime.Object, error) {
|
||||
args := f.Called(ctx, list)
|
||||
var res runtime.Object
|
||||
if args.Get(0) != nil {
|
||||
res = args.Get(0).(runtime.Object)
|
||||
}
|
||||
return res, args.Error(1)
|
||||
}
|
||||
|
||||
type fakeObject struct {
|
||||
metaV1.TypeMeta
|
||||
metaV1.ObjectMeta
|
||||
}
|
||||
|
||||
func (f *fakeObject) DeepCopyObject() runtime.Object {
|
||||
return &fakeObject{
|
||||
TypeMeta: f.TypeMeta,
|
||||
ObjectMeta: f.ObjectMeta,
|
||||
}
|
||||
}
|
||||
|
||||
// fakeUpdatedObjectInfo implements k8srest.UpdatedObjectInfo for testing
|
||||
type fakeUpdatedObjectInfo struct {
|
||||
obj runtime.Object
|
||||
}
|
||||
|
||||
func (f *fakeUpdatedObjectInfo) Preconditions() *metaV1.Preconditions {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeUpdatedObjectInfo) UpdatedObject(ctx context.Context, oldObj runtime.Object) (runtime.Object, error) {
|
||||
return f.obj, nil
|
||||
}
|
||||
@@ -782,7 +782,7 @@ func (s *Service) listPermission(ctx context.Context, scopeMap map[string]bool,
|
||||
}
|
||||
|
||||
var res *authzv1.ListResponse
|
||||
if strings.HasPrefix(req.Action, "folders:") {
|
||||
if strings.HasPrefix(req.Action, "folders:") || strings.HasPrefix(req.Action, "folders.permissions:") {
|
||||
res = buildFolderList(scopeMap, tree)
|
||||
} else {
|
||||
res = buildItemList(scopeMap, tree, t.Prefix())
|
||||
|
||||
Reference in New Issue
Block a user