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:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user