Provisioning: Use role based access when the target does not yet exist (#103862)

* role based fallback

* disable permissions cache with provisioning

* fallback to role based

* test with editor (not admin)

* test with editor (not admin)

* fix imports

* lint

* editor can create folders
This commit is contained in:
Ryan McKinley
2025-04-11 17:47:26 +03:00
committed by GitHub
parent be696dd70c
commit 664e5255fe
4 changed files with 75 additions and 73 deletions
@@ -7,8 +7,6 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/google/uuid"
authlib "github.com/grafana/authlib/types"
"github.com/grafana/grafana-app-sdk/logging"
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
@@ -48,16 +46,16 @@ func (r *DualReadWriter) Read(ctx context.Context, path string, ref string) (*Pa
return nil, fmt.Errorf("parse file: %w", err)
}
// Authorize the parsed resource
if err = r.authorize(ctx, parsed, utils.VerbGet); err != nil {
return nil, err
}
// Fail as we use the dry run for this response and it's not about updating the resource
if err := parsed.DryRun(ctx); err != nil {
return nil, fmt.Errorf("run dry run: %w", err)
}
// Authorize based on the existing resource
if err = r.authorize(ctx, parsed, utils.VerbGet); err != nil {
return nil, err
}
return parsed, nil
}
@@ -195,21 +193,6 @@ func (r *DualReadWriter) createOrUpdate(ctx context.Context, create bool, path s
return nil, err
}
// Verify that we can create (or update) the referenced resource
verb := utils.VerbUpdate
if create {
verb = utils.VerbCreate
}
if err = r.authorize(ctx, parsed, verb); err != nil {
return nil, err
}
// Always use the provisioning identity when writing
ctx, _, err = identity.WithProvisioningIdentity(ctx, parsed.Obj.GetNamespace())
if err != nil {
return nil, fmt.Errorf("unable to use provisioning identity %w", err)
}
// Make sure the value is valid
if err := parsed.DryRun(ctx); err != nil {
logger := logging.FromContext(ctx).With("path", path, "name", parsed.Obj.GetName(), "ref", ref)
@@ -224,11 +207,26 @@ func (r *DualReadWriter) createOrUpdate(ctx context.Context, create bool, path s
return nil, fmt.Errorf("errors while parsing file [%v]", parsed.Errors)
}
// Verify that we can create (or update) the referenced resource
verb := utils.VerbUpdate
if parsed.Action == provisioning.ResourceActionCreate {
verb = utils.VerbCreate
}
if err = r.authorize(ctx, parsed, verb); err != nil {
return nil, err
}
data, err = parsed.ToSaveBytes()
if err != nil {
return nil, err
}
// Always use the provisioning identity when writing
ctx, _, err = identity.WithProvisioningIdentity(ctx, parsed.Obj.GetNamespace())
if err != nil {
return nil, fmt.Errorf("unable to use provisioning identity %w", err)
}
// Create or update
if create {
err = r.repo.Create(ctx, path, ref, data, message)
@@ -255,49 +253,47 @@ func (r *DualReadWriter) createOrUpdate(ctx context.Context, create bool, path s
}
func (r *DualReadWriter) authorize(ctx context.Context, parsed *ParsedResource, verb string) error {
auth, ok := authlib.AuthInfoFrom(ctx)
if !ok {
return fmt.Errorf("missing auth info in context")
}
rsp, err := r.access.Check(ctx, auth, authlib.CheckRequest{
Group: parsed.GVR.Group,
Resource: parsed.GVR.Resource,
Namespace: parsed.Obj.GetNamespace(),
Name: parsed.Obj.GetName(),
Folder: parsed.Meta.GetFolder(),
Verb: verb,
})
id, err := identity.GetRequester(ctx)
if err != nil {
return err
return apierrors.NewUnauthorized(err.Error())
}
if !rsp.Allowed {
return apierrors.NewForbidden(parsed.GVR.GroupResource(), parsed.Obj.GetName(),
fmt.Errorf("no access to see embedded file"))
// 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"))
}
}
return nil
// Simple role based access for now
if id.GetOrgRole().Includes(identity.RoleEditor) {
return nil
}
return apierrors.NewForbidden(parsed.GVR.GroupResource(), parsed.Obj.GetName(),
fmt.Errorf("must be admin or editor to access files from provisioning"))
}
func (r *DualReadWriter) authorizeCreateFolder(ctx context.Context, _ string) error {
auth, ok := authlib.AuthInfoFrom(ctx)
if !ok {
return fmt.Errorf("missing auth info in context")
}
rsp, err := r.access.Check(ctx, auth, authlib.CheckRequest{
Group: FolderResource.Group,
Resource: FolderResource.Resource,
Namespace: r.repo.Config().GetNamespace(),
Verb: utils.VerbCreate,
// TODO: Currently this checks if you can create a new folder in root
// Ideally we should check the path and use the explicit parent and new id
Name: "f" + uuid.NewString(),
})
id, err := identity.GetRequester(ctx)
if err != nil {
return err
return apierrors.NewUnauthorized(err.Error())
}
if !rsp.Allowed {
return apierrors.NewForbidden(FolderResource.GroupResource(), "",
fmt.Errorf("unable to create folder resource"))
// Simple role based access for now
if id.GetOrgRole().Includes(identity.RoleEditor) {
return nil
}
return nil
return apierrors.NewForbidden(FolderResource.GroupResource(), "",
fmt.Errorf("must be admin or editor to access folders with provisioning"))
}
+12 -5
View File
@@ -8,11 +8,6 @@ import (
"time"
"github.com/fullstorydev/grpchan/inprocgrpc"
authnlib "github.com/grafana/authlib/authn"
authzlib "github.com/grafana/authlib/authz"
authzv1 "github.com/grafana/authlib/authz/proto/v1"
"github.com/grafana/authlib/cache"
authlib "github.com/grafana/authlib/types"
grpcAuth "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel/trace"
@@ -21,6 +16,11 @@ import (
"google.golang.org/grpc/credentials/insecure"
"k8s.io/client-go/rest"
authnlib "github.com/grafana/authlib/authn"
authzlib "github.com/grafana/authlib/authz"
authzv1 "github.com/grafana/authlib/authz/proto/v1"
"github.com/grafana/authlib/cache"
authlib "github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
@@ -60,6 +60,13 @@ func ProvideAuthZClient(
return nil, errors.New("authZGRPCServer feature toggle is required for cloud and grpc mode")
}
// Provisioning uses mode 4 (read+write only to unified storage)
// For G12 launch, we can disable caching for this and find a more scalable solution soon
// most likely this would involve passing the RV (timestamp!) in each check method
if features.IsEnabledGlobally(featuremgmt.FlagProvisioning) {
authCfg.cacheTTL = 0
}
switch authCfg.mode {
case clientModeCloud:
rbacClient, err := newRemoteRBACClient(authCfg, tracer)
+7 -8
View File
@@ -48,6 +48,7 @@ type provisioningTestHelper struct {
Folders *apis.K8sResourceClient
Dashboards *apis.K8sResourceClient
AdminREST *rest.RESTClient
EditorREST *rest.RESTClient
ViewerREST *rest.RESTClient
}
@@ -256,13 +257,10 @@ func runGrafana(t *testing.T, options ...grafanaOption) *provisioningTestHelper
})
// Repo client, but less guard rails. Useful for subresources. We'll need this later...
restClient := helper.Org1.Admin.RESTClient(t, &schema.GroupVersion{
Group: "provisioning.grafana.app", Version: "v0alpha1",
})
viewerClient := helper.Org1.Viewer.RESTClient(t, &schema.GroupVersion{
Group: "provisioning.grafana.app", Version: "v0alpha1",
})
gv := &schema.GroupVersion{Group: "provisioning.grafana.app", Version: "v0alpha1"}
adminClient := helper.Org1.Admin.RESTClient(t, gv)
editorClient := helper.Org1.Editor.RESTClient(t, gv)
viewerClient := helper.Org1.Viewer.RESTClient(t, gv)
deleteAll := func(client *apis.K8sResourceClient) error {
ctx := context.Background()
@@ -287,7 +285,8 @@ func runGrafana(t *testing.T, options ...grafanaOption) *provisioningTestHelper
K8sTestHelper: helper,
Repositories: repositories,
AdminREST: restClient,
AdminREST: adminClient,
EditorREST: editorClient,
ViewerREST: viewerClient,
Jobs: jobs,
Folders: folders,
@@ -283,8 +283,8 @@ func TestIntegrationProvisioning_RunLocalRepository(t *testing.T) {
require.Equal(t, http.StatusNotFound, code)
require.True(t, apierrors.IsNotFound(result.Error()))
// Now try again with POST
result = helper.AdminREST.Post().
// Now try again with POST (as an editor)
result = helper.EditorREST.Post().
Namespace("default").
Resource("repositories").
Name(repo).