From 42661bed36b6143f922bae4f3434137169ee86fb Mon Sep 17 00:00:00 2001 From: Costa Alexoglou Date: Thu, 4 Dec 2025 18:27:49 +0100 Subject: [PATCH] feat: add default permissions and DTO support for MT (#114829) * feat: add default permissions and DTO support for MT * chore: review comments * chore: review comments --- pkg/registry/apis/dashboard/register.go | 103 +++++++++++++++++-- pkg/registry/apis/dashboard/sub_dto.go | 127 ++++++++++++++---------- 2 files changed, 172 insertions(+), 58 deletions(-) diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index d98407a1f57..16873ec2101 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -64,6 +64,7 @@ import ( "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" "github.com/grafana/grafana/pkg/storage/unified/apistore" "github.com/grafana/grafana/pkg/storage/unified/resource" + resourcepb "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/util" ) @@ -531,11 +532,9 @@ func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver RequireDeprecatedInternalID: true, } - // TODO: merge this into one option if b.isStandalone { - // TODO: Sets default root permissions + storageOpts.Permissions = b.setDefaultDashboardPermissions } else { - // Sets default root permissions storageOpts.Permissions = b.dashboardPermissions.SetDefaultPermissionsAfterCreate } @@ -647,6 +646,18 @@ func (b *DashboardsAPIBuilder) storageForVersion( unified.AfterDelete = b.afterDelete storage[dashboards.StoragePath()] = unified + storage[dashboards.StoragePath("dto")], err = NewDTOConnector( + unified, + largeObjects, + b.unified, + b.accessClient, + newDTOFunc, + nil, // no publicDashboardService in standalone mode + ) + if err != nil { + return err + } + return nil } @@ -675,10 +686,8 @@ func (b *DashboardsAPIBuilder) storageForVersion( storage[dashboards.StoragePath("dto")], err = NewDTOConnector( storage[dashboards.StoragePath()].(rest.Getter), largeObjects, - b.legacy.Access, b.unified, - b.accessControl, - opts.Scheme, + b.accessClient, newDTOFunc, b.publicDashboardService, ) @@ -746,6 +755,88 @@ func (b *DashboardsAPIBuilder) afterDelete(obj runtime.Object, _ *metav1.DeleteO } } +var defaultDashboardPermissions = []map[string]any{ + { + "kind": "BasicRole", + "name": "Admin", + "verb": "admin", + }, + { + "kind": "BasicRole", + "name": "Editor", + "verb": "edit", + }, + { + "kind": "BasicRole", + "name": "Viewer", + "verb": "view", + }, +} + +func (b *DashboardsAPIBuilder) setDefaultDashboardPermissions(ctx context.Context, key *resourcepb.ResourceKey, id authlib.AuthInfo, obj utils.GrafanaMetaAccessor) error { + if b.resourcePermissionsSvc == nil { + return nil + } + + if obj.GetFolder() != "" { + return nil + } + + log := logging.FromContext(ctx) + log.Debug("setting default dashboard permissions", "uid", obj.GetName(), "namespace", obj.GetNamespace()) + + client := (*b.resourcePermissionsSvc).Namespace(obj.GetNamespace()) + name := fmt.Sprintf("%s-%s-%s", dashv1.DashboardResourceInfo.GroupVersionResource().Group, dashv1.DashboardResourceInfo.GroupVersionResource().Resource, obj.GetName()) + + if _, err := client.Get(ctx, name, metav1.GetOptions{}); err == nil { + _, err := client.Update(ctx, &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]any{ + "name": name, + "namespace": obj.GetNamespace(), + }, + "spec": map[string]any{ + "resource": map[string]any{ + "apiGroup": dashv1.DashboardResourceInfo.GroupVersionResource().Group, + "resource": dashv1.DashboardResourceInfo.GroupVersionResource().Resource, + "name": obj.GetName(), + }, + "permissions": defaultDashboardPermissions, + }, + }, + }, metav1.UpdateOptions{}) + if err != nil { + log.Error("failed to update dashboard permissions", "error", err) + return fmt.Errorf("update dashboard permissions: %w", err) + } + + return nil + } + + _, err := client.Create(ctx, &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]any{ + "name": name, + "namespace": obj.GetNamespace(), + }, + "spec": map[string]any{ + "resource": map[string]any{ + "apiGroup": dashv1.DashboardResourceInfo.GroupVersionResource().Group, + "resource": dashv1.DashboardResourceInfo.GroupVersionResource().Resource, + "name": obj.GetName(), + }, + "permissions": defaultDashboardPermissions, + }, + }, + }, metav1.CreateOptions{}) + if err != nil { + log.Error("failed to create dashboard permissions", "error", err) + return fmt.Errorf("create dashboard permissions: %w", err) + } + + return nil +} + func (b *DashboardsAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { return func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { defs := dashv0.GetOpenAPIDefinitions(ref) diff --git a/pkg/registry/apis/dashboard/sub_dto.go b/pkg/registry/apis/dashboard/sub_dto.go index 04a14387fc7..d774806147c 100644 --- a/pkg/registry/apis/dashboard/sub_dto.go +++ b/pkg/registry/apis/dashboard/sub_dto.go @@ -12,17 +12,17 @@ import ( authlib "github.com/grafana/authlib/types" "github.com/grafana/grafana-app-sdk/logging" "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard" + dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/infra/slugify" - "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" - "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/publicdashboards" "github.com/grafana/grafana/pkg/storage/unified/apistore" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" + "github.com/grafana/grafana/pkg/util" ) type dtoBuilder = func(dashboard runtime.Object, access *dashboard.DashboardAccess) (runtime.Object, error) @@ -30,11 +30,9 @@ type dtoBuilder = func(dashboard runtime.Object, access *dashboard.DashboardAcce // The DTO returns everything the UI needs in a single request type DTOConnector struct { getter rest.Getter - legacy legacy.DashboardAccessor unified resource.ResourceClient largeObjects apistore.LargeObjectSupport - accessControl accesscontrol.AccessControl - scheme *runtime.Scheme + accessClient authlib.AccessClient builder dtoBuilder publicDashboardService publicdashboards.Service } @@ -42,21 +40,17 @@ type DTOConnector struct { func NewDTOConnector( getter rest.Getter, largeObjects apistore.LargeObjectSupport, - legacyAccess legacy.DashboardAccessor, resourceClient resource.ResourceClient, - accessControl accesscontrol.AccessControl, - scheme *runtime.Scheme, + accessClient authlib.AccessClient, builder dtoBuilder, publicDashboardService publicdashboards.Service, ) (rest.Storage, error) { return &DTOConnector{ getter: getter, - legacy: legacyAccess, - accessControl: accessControl, + accessClient: accessClient, unified: resourceClient, largeObjects: largeObjects, builder: builder, - scheme: scheme, publicDashboardService: publicDashboardService, }, nil } @@ -132,35 +126,87 @@ func (r *DTOConnector) Connect(ctx context.Context, name string, opts runtime.Ob return } - dashScope := dashboards.ScopeDashboardsProvider.GetResourceScopeUID(name) - evaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsRead, dashScope) - canView, err := r.accessControl.Evaluate(ctx, user, evaluator) - if err != nil || !canView { + logger := logging.FromContext(ctx).With("logger", "dto-connector") + access := &dashboard.DashboardAccess{} + folder := obj.GetFolder() + ns := obj.GetNamespace() + + authInfo, ok := authlib.AuthInfoFrom(ctx) + if !ok { + responder.Error(fmt.Errorf("no identity found for request")) + return + } + + gvr := dashv1.DashboardResourceInfo.GroupVersionResource() + + // Check read permission using authlib.AccessClient + readRes, err := r.accessClient.Check(ctx, authInfo, authlib.CheckRequest{ + Verb: utils.VerbGet, + Group: gvr.Group, + Resource: gvr.Resource, + Namespace: ns, + Name: name, + }, folder) + if err != nil { + logger.Warn("Failed to check read permission", "err", err) + responder.Error(fmt.Errorf("not allowed to view")) + return + } + if !readRes.Allowed { responder.Error(fmt.Errorf("not allowed to view")) return } - access := &dashboard.DashboardAccess{} - writeEvaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashScope) - access.CanSave, _ = r.accessControl.Evaluate(ctx, user, writeEvaluator) - access.CanEdit = access.CanSave - adminEvaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsPermissionsWrite, dashScope) - access.CanAdmin, _ = r.accessControl.Evaluate(ctx, user, adminEvaluator) - deleteEvaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsDelete, dashScope) - access.CanDelete, _ = r.accessControl.Evaluate(ctx, user, deleteEvaluator) + // Check write permission + writeRes, err := r.accessClient.Check(ctx, authInfo, authlib.CheckRequest{ + Verb: utils.VerbUpdate, + Group: gvr.Group, + Resource: gvr.Resource, + Namespace: ns, + Name: name, + }, folder) + // Keeping the same logic as with accessControl.Evaluate. + // On errors we default on deny. + if err != nil { + logger.Warn("Failed to check write permission", "err", err) + } + access.CanSave = writeRes.Allowed + access.CanEdit = writeRes.Allowed + + // Check delete permission + deleteRes, err := r.accessClient.Check(ctx, authInfo, authlib.CheckRequest{ + Verb: utils.VerbDelete, + Group: gvr.Group, + Resource: gvr.Resource, + Namespace: ns, + Name: name, + }, folder) + if err != nil { + logger.Warn("Failed to check delete permission", "err", err) + } + access.CanDelete = deleteRes.Allowed + + // For admin permission, use write as a proxy for now + access.CanAdmin = writeRes.Allowed + access.CanStar = user.IsIdentityType(authlib.TypeUser) - access.AnnotationsPermissions = &dashboard.AnnotationPermission{} - r.getAnnotationPermissionsByScope(ctx, user, &access.AnnotationsPermissions.Dashboard, dashScope) - r.getAnnotationPermissionsByScope(ctx, user, &access.AnnotationsPermissions.Organization, accesscontrol.ScopeAnnotationsTypeOrganization) + // Annotation permissions - use write permission as proxy + access.AnnotationsPermissions = &dashboard.AnnotationPermission{ + Dashboard: dashboard.AnnotationActions{CanAdd: writeRes.Allowed, CanEdit: writeRes.Allowed, CanDelete: writeRes.Allowed}, + Organization: dashboard.AnnotationActions{CanAdd: writeRes.Allowed, CanEdit: writeRes.Allowed, CanDelete: writeRes.Allowed}, + } title := obj.FindTitle("") access.Slug = slugify.Slugify(title) access.Url = dashboards.GetDashboardFolderURL(false, name, access.Slug) - pubDash, err := r.publicDashboardService.FindByDashboardUid(ctx, user.GetOrgID(), name) - if err == nil && pubDash != nil { - access.IsPublic = true + // Only check public dashboards if service is available + if !util.IsInterfaceNil(r.publicDashboardService) { + pubDash, err := r.publicDashboardService.FindByDashboardUid(ctx, user.GetOrgID(), name) + if err == nil && pubDash != nil { + access.IsPublic = true + } } dash, err := r.builder(rawobj, access) @@ -171,26 +217,3 @@ func (r *DTOConnector) Connect(ctx context.Context, name string, opts runtime.Ob responder.Object(http.StatusOK, dash) }), nil } - -func (r *DTOConnector) getAnnotationPermissionsByScope(ctx context.Context, user identity.Requester, actions *dashboard.AnnotationActions, scope string) { - var err error - logger := logging.FromContext(ctx).With("logger", "dto-connector") - - evaluate := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, scope) - actions.CanAdd, err = r.accessControl.Evaluate(ctx, user, evaluate) - if err != nil { - logger.Warn("Failed to evaluate permission", "err", err, "action", accesscontrol.ActionAnnotationsCreate, "scope", scope) - } - - evaluate = accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsDelete, scope) - actions.CanDelete, err = r.accessControl.Evaluate(ctx, user, evaluate) - if err != nil { - logger.Warn("Failed to evaluate permission", "err", err, "action", accesscontrol.ActionAnnotationsDelete, "scope", scope) - } - - evaluate = accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsWrite, scope) - actions.CanEdit, err = r.accessControl.Evaluate(ctx, user, evaluate) - if err != nil { - logger.Warn("Failed to evaluate permission", "err", err, "action", accesscontrol.ActionAnnotationsWrite, "scope", scope) - } -}