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
This commit is contained in:
Costa Alexoglou
2025-12-04 18:27:49 +01:00
committed by GitHub
parent c90677831b
commit 42661bed36
2 changed files with 172 additions and 58 deletions
+97 -6
View File
@@ -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)
+75 -52
View File
@@ -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)
}
}