grafana-iam: Use parent folder to authorize ResourcePermissions (#115008)

* `grafana-iam`: Fetch target parent folder

* WIP add different ParentProviders

* Add version

* Move code to a different file

* Instantiate resourceParentProvider

* same import name

* imports

* Add tests

* Remove unecessary test

* forgot wire

* WIP integration tests

* Add test to cover list

* Fix caching problem in integration tests

* comments

* Logger and comments

* Add lazy creation and caching

* Instantiate clients only once

* Rerun wire gen
This commit is contained in:
Gabriel MABILLE
2025-12-12 11:43:12 +01:00
committed by GitHub
parent 8ebb1c2bc9
commit 478ae15f0e
8 changed files with 1136 additions and 16 deletions
@@ -0,0 +1,164 @@
package authorizer
import (
"context"
"errors"
"fmt"
"net/http"
"sync"
"github.com/grafana/authlib/authn"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
dashboardv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
folderv1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
"github.com/grafana/grafana/apps/provisioning/pkg/auth"
"github.com/grafana/grafana/pkg/apimachinery/utils"
)
var (
ErrNoConfigProvider = errors.New("no config provider for group resource")
ErrNoVersionInfo = errors.New("no version info for group resource")
Versions = map[schema.GroupResource]string{
{Group: folderv1.GROUP, Resource: folderv1.RESOURCE}: folderv1.VERSION,
{Group: dashboardv1.GROUP, Resource: dashboardv1.DASHBOARD_RESOURCE}: dashboardv1.VERSION,
}
)
// ConfigProvider is a function that provides a rest.Config for a given context.
type ConfigProvider func(ctx context.Context) (*rest.Config, error)
// DynamicClientFactory is a function that creates a dynamic.Interface from a rest.Config.
// This can be overridden in tests.
type DynamicClientFactory func(config *rest.Config) (dynamic.Interface, error)
// ParentProvider implementation that fetches the parent folder information from remote API servers.
type ParentProviderImpl struct {
configProviders map[schema.GroupResource]ConfigProvider
versions map[schema.GroupResource]string
dynamicClientFactory DynamicClientFactory
// Cache of dynamic clients for each group resource
// This is used to avoid creating a new dynamic client for each request
// and to reuse the same client for the same group resource.
clients map[schema.GroupResource]dynamic.Interface
clientsMu sync.Mutex
}
// DialConfig holds the configuration for dialing a remote API server.
type DialConfig struct {
Host string
Insecure bool
CAFile string
Audience string
}
// NewLocalConfigProvider creates a map of ConfigProviders that return the same given config for local API servers.
func NewLocalConfigProvider(
configProvider ConfigProvider,
) map[schema.GroupResource]ConfigProvider {
return map[schema.GroupResource]ConfigProvider{
{Group: folderv1.GROUP, Resource: folderv1.RESOURCE}: configProvider,
{Group: dashboardv1.GROUP, Resource: dashboardv1.DASHBOARD_RESOURCE}: configProvider,
}
}
// NewRemoteConfigProvider creates a map of ConfigProviders for remote API servers based on the given DialConfig.
func NewRemoteConfigProvider(cfg map[schema.GroupResource]DialConfig, exchangeClient authn.TokenExchanger) map[schema.GroupResource]ConfigProvider {
configProviders := make(map[schema.GroupResource]ConfigProvider, len(cfg))
for gr, dialConfig := range cfg {
configProviders[gr] = func(ctx context.Context) (*rest.Config, error) {
return &rest.Config{
Host: dialConfig.Host,
WrapTransport: func(rt http.RoundTripper) http.RoundTripper {
return auth.NewRoundTripper(exchangeClient, rt, dialConfig.Audience)
},
TLSClientConfig: rest.TLSClientConfig{
Insecure: dialConfig.Insecure,
CAFile: dialConfig.CAFile,
},
QPS: 50,
Burst: 100,
}, nil
}
}
return configProviders
}
// NewApiParentProvider creates a new ParentProviderImpl with the given config providers and version info.
func NewApiParentProvider(
configProviders map[schema.GroupResource]ConfigProvider,
version map[schema.GroupResource]string,
) *ParentProviderImpl {
return &ParentProviderImpl{
configProviders: configProviders,
versions: version,
dynamicClientFactory: func(config *rest.Config) (dynamic.Interface, error) {
return dynamic.NewForConfig(config)
},
clients: make(map[schema.GroupResource]dynamic.Interface),
}
}
func (p *ParentProviderImpl) HasParent(gr schema.GroupResource) bool {
_, ok := p.configProviders[gr]
return ok
}
func (p *ParentProviderImpl) getClient(ctx context.Context, gr schema.GroupResource) (dynamic.Interface, error) {
p.clientsMu.Lock()
client, ok := p.clients[gr]
p.clientsMu.Unlock()
if ok {
return client, nil
}
provider, ok := p.configProviders[gr]
if !ok {
return nil, fmt.Errorf("%w: %s", ErrNoConfigProvider, gr.String())
}
restConfig, err := provider(ctx)
if err != nil {
return nil, err
}
client, err = p.dynamicClientFactory(restConfig)
if err != nil {
return nil, err
}
p.clientsMu.Lock()
p.clients[gr] = client
p.clientsMu.Unlock()
return client, nil
}
func (p *ParentProviderImpl) GetParent(ctx context.Context, gr schema.GroupResource, namespace, name string) (string, error) {
client, err := p.getClient(ctx, gr)
if err != nil {
return "", err
}
version, ok := p.versions[gr]
if !ok {
return "", fmt.Errorf("%w: %s", ErrNoVersionInfo, gr.String())
}
resourceClient := client.Resource(schema.GroupVersionResource{
Group: gr.Group,
Resource: gr.Resource,
Version: version,
}).Namespace(namespace)
unstructObj, err := resourceClient.Get(ctx, name, metav1.GetOptions{})
if err != nil {
return "", err
}
return unstructObj.GetAnnotations()[utils.AnnoKeyFolder], nil
}
@@ -0,0 +1,198 @@
package authorizer
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
folderv1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
)
var configProvider = func(ctx context.Context) (*rest.Config, error) {
return &rest.Config{}, nil
}
func TestParentProviderImpl_GetParent(t *testing.T) {
tests := []struct {
name string
gr schema.GroupResource
namespace string
resourceName string
parentFolder string
setupFake func(*fakeDynamicClient, *fakeResourceInterface)
configProviders map[schema.GroupResource]ConfigProvider
versions map[schema.GroupResource]string
expectedError string
expectedParent string
}{
{
name: "successfully get parent folder",
gr: schema.GroupResource{Group: folderv1.GROUP, Resource: folderv1.RESOURCE},
namespace: "org-1",
resourceName: "dash1",
parentFolder: "fold1",
setupFake: func(fakeClient *fakeDynamicClient, fakeResource *fakeResourceInterface) {
fakeClient.resourceInterface = fakeResource
fakeResource.getFunc = func(ctx context.Context, name string, opts metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) {
obj := &unstructured.Unstructured{}
obj.SetAnnotations(map[string]string{utils.AnnoKeyFolder: "fold1"})
return obj, nil
}
},
configProviders: map[schema.GroupResource]ConfigProvider{
{Group: folderv1.GROUP, Resource: folderv1.RESOURCE}: configProvider,
},
versions: Versions,
expectedParent: "fold1",
},
{
name: "resource without parent annotation returns empty",
gr: schema.GroupResource{Group: folderv1.GROUP, Resource: folderv1.RESOURCE},
namespace: "org-1",
resourceName: "dash1",
setupFake: func(fakeClient *fakeDynamicClient, fakeResource *fakeResourceInterface) {
fakeClient.resourceInterface = fakeResource
fakeResource.getFunc = func(ctx context.Context, name string, opts metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) {
obj := &unstructured.Unstructured{}
obj.SetAnnotations(map[string]string{})
return obj, nil
}
},
configProviders: map[schema.GroupResource]ConfigProvider{
{Group: folderv1.GROUP, Resource: folderv1.RESOURCE}: configProvider,
},
versions: Versions,
expectedParent: "",
},
{
name: "no config provider returns error",
gr: schema.GroupResource{Group: "unknown.group", Resource: "unknown"},
namespace: "org-1",
resourceName: "resource-1",
configProviders: map[schema.GroupResource]ConfigProvider{},
versions: Versions,
expectedError: ErrNoConfigProvider.Error(),
},
{
name: "config provider returns error",
gr: schema.GroupResource{Group: folderv1.GROUP, Resource: folderv1.RESOURCE},
namespace: "org-1",
resourceName: "resource-1",
configProviders: map[schema.GroupResource]ConfigProvider{
{Group: folderv1.GROUP, Resource: folderv1.RESOURCE}: func(ctx context.Context) (*rest.Config, error) {
return nil, errors.New("config provider error")
},
},
versions: Versions,
expectedError: "config provider error",
},
{
name: "no version info returns error",
gr: schema.GroupResource{Group: folderv1.GROUP, Resource: folderv1.RESOURCE},
namespace: "org-1",
resourceName: "resource-1",
configProviders: map[schema.GroupResource]ConfigProvider{
{Group: folderv1.GROUP, Resource: folderv1.RESOURCE}: func(ctx context.Context) (*rest.Config, error) {
return &rest.Config{}, nil
},
},
versions: map[schema.GroupResource]string{},
expectedError: ErrNoVersionInfo.Error(),
},
{
name: "resource get returns error",
gr: schema.GroupResource{Group: folderv1.GROUP, Resource: folderv1.RESOURCE},
namespace: "org-1",
resourceName: "resource-1",
setupFake: func(fakeClient *fakeDynamicClient, fakeResource *fakeResourceInterface) {
fakeClient.resourceInterface = fakeResource
fakeResource.getFunc = func(ctx context.Context, name string, opts metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) {
return nil, errors.New("resource not found")
}
},
configProviders: map[schema.GroupResource]ConfigProvider{
{Group: folderv1.GROUP, Resource: folderv1.RESOURCE}: func(ctx context.Context) (*rest.Config, error) {
return &rest.Config{}, nil
},
},
versions: Versions,
expectedError: "resource not found",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fakeClient := &fakeDynamicClient{}
fakeResource := &fakeResourceInterface{}
if tt.setupFake != nil {
tt.setupFake(fakeClient, fakeResource)
}
provider := &ParentProviderImpl{
configProviders: tt.configProviders,
versions: tt.versions,
dynamicClientFactory: func(config *rest.Config) (dynamic.Interface, error) {
return fakeClient, nil
},
clients: make(map[schema.GroupResource]dynamic.Interface),
}
parent, err := provider.GetParent(context.Background(), tt.gr, tt.namespace, tt.resourceName)
if tt.expectedError != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.expectedError)
assert.Empty(t, parent)
} else {
require.NoError(t, err)
assert.Equal(t, tt.expectedParent, parent)
}
})
}
}
// fakeDynamicClient is a fake implementation of dynamic.Interface
type fakeDynamicClient struct {
resourceInterface dynamic.ResourceInterface
}
func (f *fakeDynamicClient) Resource(resource schema.GroupVersionResource) dynamic.NamespaceableResourceInterface {
return &fakeNamespaceableResourceInterface{
resourceInterface: f.resourceInterface,
}
}
// fakeNamespaceableResourceInterface is a fake implementation of dynamic.NamespaceableResourceInterface
type fakeNamespaceableResourceInterface struct {
dynamic.NamespaceableResourceInterface
resourceInterface dynamic.ResourceInterface
}
func (f *fakeNamespaceableResourceInterface) Namespace(namespace string) dynamic.ResourceInterface {
if f.resourceInterface != nil {
return f.resourceInterface
}
return &fakeResourceInterface{}
}
// fakeResourceInterface is a fake implementation of dynamic.ResourceInterface
type fakeResourceInterface struct {
dynamic.ResourceInterface
getFunc func(ctx context.Context, name string, opts metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error)
}
func (f *fakeResourceInterface) Get(ctx context.Context, name string, opts metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) {
if f.getFunc != nil {
return f.getFunc(ctx, name, opts, subresources...)
}
return &unstructured.Unstructured{}, nil
}
@@ -10,24 +10,44 @@ import (
iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer/storewrapper"
)
// TODO: Logs, Metrics, Traces?
// ParentProvider interface for fetching parent information of resources
type ParentProvider interface {
// HasParent checks if the given GroupResource has a parent folder
HasParent(gr schema.GroupResource) bool
// GetParent fetches the parent folder name for the given resource
GetParent(ctx context.Context, gr schema.GroupResource, namespace, name string) (string, error)
}
// ResourcePermissionsAuthorizer
type ResourcePermissionsAuthorizer struct {
accessClient types.AccessClient
accessClient types.AccessClient
parentProvider ParentProvider
logger log.Logger
}
var _ storewrapper.ResourceStorageAuthorizer = (*ResourcePermissionsAuthorizer)(nil)
func NewResourcePermissionsAuthorizer(accessClient types.AccessClient) *ResourcePermissionsAuthorizer {
func NewResourcePermissionsAuthorizer(
accessClient types.AccessClient,
parentProvider ParentProvider,
) *ResourcePermissionsAuthorizer {
return &ResourcePermissionsAuthorizer{
accessClient: accessClient,
accessClient: accessClient,
parentProvider: parentProvider,
logger: log.New("iam.resource-permissions-authorizer"),
}
}
func isAccessPolicy(authInfo types.AuthInfo) bool {
return types.IsIdentityType(authInfo.GetIdentityType(), types.TypeAccessPolicy)
}
// AfterGet implements ResourceStorageAuthorizer.
func (r *ResourcePermissionsAuthorizer) AfterGet(ctx context.Context, obj runtime.Object) error {
authInfo, ok := types.AuthInfoFrom(ctx)
@@ -37,9 +57,24 @@ func (r *ResourcePermissionsAuthorizer) AfterGet(ctx context.Context, obj runtim
switch o := obj.(type) {
case *iamv0.ResourcePermission:
target := o.Spec.Resource
targetGR := schema.GroupResource{Group: target.ApiGroup, Resource: target.Resource}
// TODO: Fetch the resource to retrieve its parent folder.
parent := ""
// Fetch the parent of the resource
// Access Policies have global scope, so no parent check needed
if !isAccessPolicy(authInfo) && r.parentProvider.HasParent(targetGR) {
p, err := r.parentProvider.GetParent(ctx, targetGR, o.Namespace, target.Name)
if err != nil {
r.logger.Error("after get: error fetching parent", "error", err.Error(),
"namespace", o.Namespace,
"group", target.ApiGroup,
"resource", target.Resource,
"name", target.Name,
)
return err
}
parent = p
}
checkReq := types.CheckRequest{
Namespace: o.Namespace,
@@ -72,9 +107,24 @@ func (r *ResourcePermissionsAuthorizer) beforeWrite(ctx context.Context, obj run
switch o := obj.(type) {
case *iamv0.ResourcePermission:
target := o.Spec.Resource
targetGR := schema.GroupResource{Group: target.ApiGroup, Resource: target.Resource}
// TODO: Fetch the resource to retrieve its parent folder.
parent := ""
// Fetch the parent of the resource
// Access Policies have global scope, so no parent check needed
if !isAccessPolicy(authInfo) && r.parentProvider.HasParent(targetGR) {
p, err := r.parentProvider.GetParent(ctx, targetGR, o.Namespace, target.Name)
if err != nil {
r.logger.Error("before write: error fetching parent", "error", err.Error(),
"namespace", o.Namespace,
"group", target.ApiGroup,
"resource", target.Resource,
"name", target.Name,
)
return err
}
parent = p
}
checkReq := types.CheckRequest{
Namespace: o.Namespace,
@@ -153,8 +203,29 @@ func (r *ResourcePermissionsAuthorizer) FilterList(ctx context.Context, list run
canViewFuncs[gr] = canView
}
// TODO : Fetch the resource to retrieve its parent folder.
target := item.Spec.Resource
targetGR := schema.GroupResource{Group: target.ApiGroup, Resource: target.Resource}
parent := ""
// Fetch the parent of the resource
// It's not efficient to do for every item in the list, but it's a good starting point.
// Access Policies have global scope, so no parent check needed
if !isAccessPolicy(authInfo) && r.parentProvider.HasParent(targetGR) {
p, err := r.parentProvider.GetParent(ctx, targetGR, item.Namespace, target.Name)
if err != nil {
// Skip item on error fetching parent
r.logger.Warn("filter list: error fetching parent, skipping item",
"error", err.Error(),
"namespace",
item.Namespace,
"group", target.ApiGroup,
"resource", target.Resource,
"name", target.Name,
)
continue
}
parent = p
}
allowed := canView(item.Spec.Resource.Name, parent)
if allowed {
@@ -5,13 +5,15 @@ import (
"testing"
"github.com/go-jose/go-jose/v4/jwt"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"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 (
@@ -63,6 +65,7 @@ func TestResourcePermissions_AfterGet(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parent := "fold-1"
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
@@ -74,12 +77,18 @@ func TestResourcePermissions_AfterGet(t *testing.T) {
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)
require.Equal(t, parent, folder)
return types.CheckResponse{Allowed: tt.shouldAllow}, nil
}
getParentFunc := func(ctx context.Context, gr schema.GroupResource, namespace, name string) (string, error) {
// For this test, we can return a fixed parent folder ID
return parent, nil
}
accessClient := &fakeAccessClient{checkFunc: checkFunc}
resPermAuthz := NewResourcePermissionsAuthorizer(accessClient)
fakeParentProvider := &fakeParentProvider{hasParent: true, getParentFunc: getParentFunc}
resPermAuthz := NewResourcePermissionsAuthorizer(accessClient, fakeParentProvider)
ctx := types.WithAuthInfo(context.Background(), user)
err := resPermAuthz.AfterGet(ctx, fold1)
@@ -89,6 +98,7 @@ func TestResourcePermissions_AfterGet(t *testing.T) {
require.Error(t, err, "expected error for denied access")
}
require.True(t, accessClient.checkCalled, "accessClient.Check should be called")
require.True(t, fakeParentProvider.getParentCalled, "parentProvider.GetParent should be called")
})
}
}
@@ -121,23 +131,32 @@ func TestResourcePermissions_FilterList(t *testing.T) {
require.Equal(t, "dashboards", req.Resource)
}
// Return a checker that allows only specific resources: fold-1 and dash-2
// Return a checker that allows access to fold-1 and its content
return func(name, folder string) bool {
if name == "fold-1" || name == "dash-2" {
if name == "fold-1" || folder == "fold-1" {
return true
}
return false
}, &types.NoopZookie{}, nil
}
getParentFunc := func(ctx context.Context, gr schema.GroupResource, namespace, name string) (string, error) {
if name == "dash-2" {
return "fold-1", nil
}
return "", nil
}
accessClient := &fakeAccessClient{compileFunc: compileFunc}
resPermAuthz := NewResourcePermissionsAuthorizer(accessClient)
fakeParentProvider := &fakeParentProvider{hasParent: true, getParentFunc: getParentFunc}
resPermAuthz := NewResourcePermissionsAuthorizer(accessClient, fakeParentProvider)
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")
require.True(t, fakeParentProvider.getParentCalled, "parentProvider.GetParent should be called")
filtered, ok := obj.(*iamv0.ResourcePermissionList)
require.True(t, ok, "response should be of type ResourcePermissionList")
@@ -165,6 +184,7 @@ func TestResourcePermissions_beforeWrite(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parent := "fold-1"
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
@@ -176,12 +196,18 @@ func TestResourcePermissions_beforeWrite(t *testing.T) {
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)
require.Equal(t, parent, folder)
return types.CheckResponse{Allowed: tt.shouldAllow}, nil
}
getParentFunc := func(ctx context.Context, gr schema.GroupResource, namespace, name string) (string, error) {
return parent, nil
}
accessClient := &fakeAccessClient{checkFunc: checkFunc}
resPermAuthz := NewResourcePermissionsAuthorizer(accessClient)
fakeParentProvider := &fakeParentProvider{hasParent: true, getParentFunc: getParentFunc}
resPermAuthz := NewResourcePermissionsAuthorizer(accessClient, fakeParentProvider)
ctx := types.WithAuthInfo(context.Background(), user)
err := resPermAuthz.beforeWrite(ctx, fold1)
@@ -191,6 +217,7 @@ func TestResourcePermissions_beforeWrite(t *testing.T) {
require.Error(t, err, "expected error for denied delete")
}
require.True(t, accessClient.checkCalled, "accessClient.Check should be called")
require.True(t, fakeParentProvider.getParentCalled, "parentProvider.GetParent should be called")
})
}
}
@@ -214,3 +241,18 @@ func (m *fakeAccessClient) Compile(ctx context.Context, id types.AuthInfo, req t
}
var _ types.AccessClient = (*fakeAccessClient)(nil)
type fakeParentProvider struct {
hasParent bool
getParentCalled bool
getParentFunc func(ctx context.Context, gr schema.GroupResource, namespace, name string) (string, error)
}
func (f *fakeParentProvider) HasParent(gr schema.GroupResource) bool {
return f.hasParent
}
func (f *fakeParentProvider) GetParent(ctx context.Context, gr schema.GroupResource, namespace, name string) (string, error) {
f.getParentCalled = true
return f.getParentFunc(ctx, gr, namespace, name)
}
+5
View File
@@ -7,6 +7,7 @@ import (
"github.com/grafana/authlib/types"
"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/serviceaccount"
@@ -60,6 +61,10 @@ type IdentityAccessManagementAPIBuilder struct {
roleBindingsStorage RoleBindingStorageBackend
externalGroupMappingStorage ExternalGroupMappingStorageBackend
// Required for resource permissions authorization
// fetches resources parent folders
resourceParentProvider iamauthorizer.ParentProvider
// Access Control
authorizer authorizer.Authorizer
// legacyAccessClient is used for the identity apis, we need to migrate to the access client
+16 -1
View File
@@ -41,6 +41,7 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/iam/teambinding"
"github.com/grafana/grafana/pkg/registry/apis/iam/user"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apiserver"
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"
@@ -80,6 +81,7 @@ func RegisterAPIService(
orgService org.Service,
userService legacyuser.Service,
teamService teamservice.Service,
restConfig apiserver.RestConfigProvider,
) (*IdentityAccessManagementAPIBuilder, error) {
dbProvider := legacysql.NewDatabaseProvider(sql)
store := legacy.NewLegacySQLStores(dbProvider)
@@ -90,6 +92,11 @@ func RegisterAPIService(
//nolint:staticcheck // not yet migrated to OpenFeature
enableAuthnMutation := features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthnMutation)
resourceParentProvider := iamauthorizer.NewApiParentProvider(
iamauthorizer.NewLocalConfigProvider(restConfig.GetRestConfig),
iamauthorizer.Versions,
)
builder := &IdentityAccessManagementAPIBuilder{
store: store,
userLegacyStore: user.NewLegacyStore(store, accessClient, enableAuthnMutation, tracing),
@@ -104,6 +111,7 @@ func RegisterAPIService(
externalGroupMappingStorage: externalGroupMappingStorageBackend,
teamGroupsHandler: teamGroupsHandlerImpl,
sso: ssoService,
resourceParentProvider: resourceParentProvider,
authorizer: authorizer,
legacyAccessClient: legacyAccessClient,
accessClient: accessClient,
@@ -142,6 +150,12 @@ func NewAPIService(
resourceAuthorizer := gfauthorizer.NewResourceAuthorizer(accessClient)
coreRoleAuthorizer := iamauthorizer.NewCoreRoleAuthorizer(accessClient)
// TODO: in a follow up PR, make this configurable
resourceParentProvider := iamauthorizer.NewApiParentProvider(
iamauthorizer.NewRemoteConfigProvider(map[schema.GroupResource]iamauthorizer.DialConfig{}, nil),
iamauthorizer.Versions,
)
return &IdentityAccessManagementAPIBuilder{
store: store,
display: user.NewLegacyDisplayREST(store),
@@ -152,6 +166,7 @@ func NewAPIService(
logger: log.New("iam.apis"),
features: features,
accessClient: accessClient,
resourceParentProvider: resourceParentProvider,
zClient: zClient,
zTickets: make(chan bool, MaxConcurrentZanzanaWrites),
reg: reg,
@@ -444,7 +459,7 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateResourcePermissionsAPIGroup(
return fmt.Errorf("expected RegistryStoreDualWrite, got %T", dw)
}
authzWrapper := storewrapper.New(regStoreDW, iamauthorizer.NewResourcePermissionsAuthorizer(b.accessClient))
authzWrapper := storewrapper.New(regStoreDW, iamauthorizer.NewResourcePermissionsAuthorizer(b.accessClient, b.resourceParentProvider))
storage[iamv0.ResourcePermissionInfo.StoragePath()] = authzWrapper
return nil