From 76b3d855c7c22e84a1ecb57e5f2f919339386a0f Mon Sep 17 00:00:00 2001 From: Charandas <542168+charandas@users.noreply.github.com> Date: Thu, 18 Sep 2025 16:35:40 -0700 Subject: [PATCH] fix: use multi client for provisioning API builder (#111144) --- pkg/registry/apis/provisioning/register.go | 10 +++- .../apis/provisioning/resources/dualwriter.go | 49 ++++++++++++------- 2 files changed, 41 insertions(+), 18 deletions(-) diff --git a/pkg/registry/apis/provisioning/register.go b/pkg/registry/apis/provisioning/register.go index 69c69fdbf22..970f2e03e17 100644 --- a/pkg/registry/apis/provisioning/register.go +++ b/pkg/registry/apis/provisioning/register.go @@ -28,6 +28,7 @@ import ( authlib "github.com/grafana/authlib/types" "github.com/grafana/grafana-app-sdk/logging" + dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" @@ -132,8 +133,14 @@ func NewAPIBuilder( extraWorkers []jobs.Worker, jobHistoryConfig *JobHistoryConfig, allowedTargets []provisioning.SyncTargetType, + newStandaloneClientFactoryFunc func(loopbackConfigProvider apiserver.RestConfigProvider) resources.ClientFactory, // optional, only used for standalone apiserver ) *APIBuilder { - clients := resources.NewClientFactory(configProvider) + var clients resources.ClientFactory + if newStandaloneClientFactoryFunc != nil { + clients = newStandaloneClientFactoryFunc(configProvider) + } else { + clients = resources.NewClientFactory(configProvider) + } parsers := resources.NewParserFactory(clients) resourceLister := resources.NewResourceListerForMigrations(unified, legacyMigrator, storageStatus) @@ -237,6 +244,7 @@ func RegisterAPIService( extraWorkers, createJobHistoryConfigFromSettings(cfg), allowedTargets, + nil, ) apiregistration.RegisterAPI(builder) return builder, nil diff --git a/pkg/registry/apis/provisioning/resources/dualwriter.go b/pkg/registry/apis/provisioning/resources/dualwriter.go index fe8ad152f9b..b9af3416364 100644 --- a/pkg/registry/apis/provisioning/resources/dualwriter.go +++ b/pkg/registry/apis/provisioning/resources/dualwriter.go @@ -18,7 +18,14 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" ) -// DualReadWriter is a wrapper around a repository that can read and write resources +// DualReadWriter is a wrapper around a repository that can read from and write resources +// into both the Git repository as well as in Grafana. It isn't a dual writer in the sense of what unistore handling calls dual writing. + +// Standard provisioning Authorizer has already run by the time DualReadWriter is called +// for incoming requests from actors, external or internal. However, since it is the files +// connector that redirects here, the external resources such as dashboards +// end up requiring additional authorization checks which the DualReadWriter performs here. + // TODO: it does not support folders yet type DualReadWriter struct { repo repository.ReaderWriter @@ -496,24 +503,32 @@ func (r *DualReadWriter) authorize(ctx context.Context, parsed *ParsedResource, return apierrors.NewUnauthorized(err.Error()) } - // Use configured permissions for get+delete - if parsed.Existing != nil && (verb == utils.VerbGet || verb == utils.VerbDelete) { - rsp, err := r.access.Check(ctx, id, authlib.CheckRequest{ - Group: parsed.GVR.Group, - Resource: parsed.GVR.Resource, - Namespace: parsed.Existing.GetNamespace(), - Name: parsed.Existing.GetName(), - Folder: parsed.Meta.GetFolder(), - Verb: utils.VerbGet, - }) - if err != nil || !rsp.Allowed { - return apierrors.NewForbidden(parsed.GVR.GroupResource(), parsed.Obj.GetName(), - fmt.Errorf("no access to read the embedded file")) - } + var name string + if parsed.Existing != nil { + name = parsed.Existing.GetName() + } else { + name = parsed.Obj.GetName() } - // Simple role based access for now - if id.GetOrgRole().Includes(identity.RoleEditor) { + rsp, err := r.access.Check(ctx, id, authlib.CheckRequest{ + Group: parsed.GVR.Group, + Resource: parsed.GVR.Resource, + Namespace: id.GetNamespace(), + Name: name, + Folder: parsed.Meta.GetFolder(), + Verb: verb, + }) + if err != nil || !rsp.Allowed { + return apierrors.NewForbidden(parsed.GVR.GroupResource(), parsed.Obj.GetName(), + fmt.Errorf("no access to read the embedded file")) + } + + idType, _, err := authlib.ParseTypeID(id.GetID()) + if err != nil { + return apierrors.NewForbidden(parsed.GVR.GroupResource(), parsed.Obj.GetName(), fmt.Errorf("could not determine identity type to check access")) + } + // only apply role based access if identity is not of type access policy + if idType == authlib.TypeAccessPolicy || id.GetOrgRole().Includes(identity.RoleEditor) { return nil }