RBAC: Return 404 instead of 403 if a dashboard cannot be found (#102815)

return 404 instead of 403 if a dashboard cannot be found
pull/102694/head^2
Ieva 2 months ago committed by GitHub
parent fe1f5bc72b
commit ff6039567b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      pkg/api/apierrors/dashboard.go
  2. 3
      pkg/api/apierrors/folder.go
  3. 5
      pkg/api/apierrors/folder_test.go
  4. 9
      pkg/api/dashboard.go
  5. 12
      pkg/services/accesscontrol/middleware.go
  6. 31
      pkg/services/dashboards/dashboardaccess/dashboard_access.go
  7. 81
      pkg/services/dashboards/errors.go
  8. 3
      pkg/services/publicdashboards/service/service.go

@ -9,6 +9,7 @@ import (
"github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/services/apiserver" "github.com/grafana/grafana/pkg/services/apiserver"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
@ -16,7 +17,7 @@ import (
// ToDashboardErrorResponse returns a different response status according to the dashboard error type // ToDashboardErrorResponse returns a different response status according to the dashboard error type
func ToDashboardErrorResponse(ctx context.Context, pluginStore pluginstore.Store, err error) response.Response { func ToDashboardErrorResponse(ctx context.Context, pluginStore pluginstore.Store, err error) response.Response {
var dashboardErr dashboards.DashboardErr var dashboardErr dashboardaccess.DashboardErr
if ok := errors.As(err, &dashboardErr); ok { if ok := errors.As(err, &dashboardErr); ok {
if body := dashboardErr.Body(); body != nil { if body := dashboardErr.Body(); body != nil {
return response.JSON(dashboardErr.StatusCode, body) return response.JSON(dashboardErr.StatusCode, body)

@ -10,13 +10,14 @@ import (
"github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
"github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
// ToFolderErrorResponse returns a different response status according to the folder error type // ToFolderErrorResponse returns a different response status according to the folder error type
func ToFolderErrorResponse(err error) response.Response { func ToFolderErrorResponse(err error) response.Response {
var dashboardErr dashboards.DashboardErr var dashboardErr dashboardaccess.DashboardErr
if ok := errors.As(err, &dashboardErr); ok { if ok := errors.As(err, &dashboardErr); ok {
return response.Error(dashboardErr.StatusCode, err.Error(), err) return response.Error(dashboardErr.StatusCode, err.Error(), err)
} }

@ -7,6 +7,7 @@ import (
"github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
"github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -22,8 +23,8 @@ func TestToFolderErrorResponse(t *testing.T) {
}{ }{
{ {
name: "dashboard error", name: "dashboard error",
input: dashboards.DashboardErr{StatusCode: 400, Reason: "Dashboard Error", Status: "error"}, input: dashboardaccess.DashboardErr{StatusCode: 400, Reason: "Dashboard Error", Status: "error"},
want: response.Error(400, "Dashboard Error", dashboards.DashboardErr{StatusCode: 400, Reason: "Dashboard Error", Status: "error"}), want: response.Error(400, "Dashboard Error", dashboardaccess.DashboardErr{StatusCode: 400, Reason: "Dashboard Error", Status: "error"}),
}, },
{ {
name: "folder title empty", name: "folder title empty",

@ -24,6 +24,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
dashver "github.com/grafana/grafana/pkg/services/dashboardversion" dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
"github.com/grafana/grafana/pkg/services/dashboardversion/dashverimpl" "github.com/grafana/grafana/pkg/services/dashboardversion/dashverimpl"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
@ -65,7 +66,7 @@ func (hs *HTTPServer) isDashboardStarredByUser(c *contextmodel.ReqContext, dashI
func dashboardGuardianResponse(err error) response.Response { func dashboardGuardianResponse(err error) response.Response {
if err != nil { if err != nil {
var dashboardErr dashboards.DashboardErr var dashboardErr dashboardaccess.DashboardErr
if ok := errors.As(err, &dashboardErr); ok { if ok := errors.As(err, &dashboardErr); ok {
return response.Error(dashboardErr.StatusCode, dashboardErr.Error(), err) return response.Error(dashboardErr.StatusCode, dashboardErr.Error(), err)
} }
@ -372,7 +373,7 @@ func (hs *HTTPServer) RestoreDeletedDashboard(c *contextmodel.ReqContext) respon
err = hs.DashboardService.RestoreDashboard(c.Req.Context(), dash, c.SignedInUser, cmd.FolderUID) err = hs.DashboardService.RestoreDashboard(c.Req.Context(), dash, c.SignedInUser, cmd.FolderUID)
if err != nil { if err != nil {
var dashboardErr dashboards.DashboardErr var dashboardErr dashboardaccess.DashboardErr
if ok := errors.As(err, &dashboardErr); ok { if ok := errors.As(err, &dashboardErr); ok {
return response.Error(dashboardErr.StatusCode, dashboardErr.Error(), err) return response.Error(dashboardErr.StatusCode, dashboardErr.Error(), err)
} }
@ -411,7 +412,7 @@ func (hs *HTTPServer) SoftDeleteDashboard(c *contextmodel.ReqContext) response.R
err := hs.DashboardService.SoftDeleteDashboard(c.Req.Context(), c.SignedInUser.GetOrgID(), uid) err := hs.DashboardService.SoftDeleteDashboard(c.Req.Context(), c.SignedInUser.GetOrgID(), uid)
if err != nil { if err != nil {
var dashboardErr dashboards.DashboardErr var dashboardErr dashboardaccess.DashboardErr
if ok := errors.As(err, &dashboardErr); ok { if ok := errors.As(err, &dashboardErr); ok {
if errors.Is(err, dashboards.ErrDashboardCannotDeleteProvisionedDashboard) { if errors.Is(err, dashboards.ErrDashboardCannotDeleteProvisionedDashboard) {
return response.Error(dashboardErr.StatusCode, dashboardErr.Error(), err) return response.Error(dashboardErr.StatusCode, dashboardErr.Error(), err)
@ -497,7 +498,7 @@ func (hs *HTTPServer) deleteDashboard(c *contextmodel.ReqContext) response.Respo
err = hs.DashboardService.DeleteDashboard(c.Req.Context(), dash.ID, dash.UID, c.SignedInUser.GetOrgID()) err = hs.DashboardService.DeleteDashboard(c.Req.Context(), dash.ID, dash.UID, c.SignedInUser.GetOrgID())
if err != nil { if err != nil {
var dashboardErr dashboards.DashboardErr var dashboardErr dashboardaccess.DashboardErr
if ok := errors.As(err, &dashboardErr); ok { if ok := errors.As(err, &dashboardErr); ok {
if errors.Is(err, dashboards.ErrDashboardCannotDeleteProvisionedDashboard) { if errors.Is(err, dashboards.ErrDashboardCannotDeleteProvisionedDashboard) {
return response.Error(dashboardErr.StatusCode, dashboardErr.Error(), err) return response.Error(dashboardErr.StatusCode, dashboardErr.Error(), err)

@ -20,6 +20,7 @@ import (
"github.com/grafana/grafana/pkg/models/usertoken" "github.com/grafana/grafana/pkg/models/usertoken"
"github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authn"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
"github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
@ -87,6 +88,17 @@ func deny(c *contextmodel.ReqContext, evaluator Evaluator, err error) {
id := newID() id := newID()
if err != nil { if err != nil {
c.Logger.Error("Error from access control system", "error", err, "accessErrorID", id) c.Logger.Error("Error from access control system", "error", err, "accessErrorID", id)
// Return 404s for dashboard not found errors, our plugins rely on being able to distinguish between access denied and not found.
var dashboardErr dashboardaccess.DashboardErr
if ok := errors.As(err, &dashboardErr); ok {
if c.IsApiRequest() && dashboardErr.StatusCode == http.StatusNotFound {
c.JSON(http.StatusNotFound, map[string]string{
"title": "Not found", // the component needs to pick this up
"message": dashboardErr.Error(),
})
return
}
}
} else { } else {
c.Logger.Info( c.Logger.Info(
"Access denied", "Access denied",

@ -2,6 +2,8 @@ package dashboardaccess
import ( import (
"errors" "errors"
"github.com/grafana/grafana/pkg/util"
) )
type PermissionType int type PermissionType int
@ -30,3 +32,32 @@ var (
ErrPermissionsWithRoleNotAllowed = errors.New("permissions cannot have both a user and team") ErrPermissionsWithRoleNotAllowed = errors.New("permissions cannot have both a user and team")
ErrPermissionsWithUserAndTeamNotAllowed = errors.New("team and user permissions cannot have an associated role") ErrPermissionsWithUserAndTeamNotAllowed = errors.New("team and user permissions cannot have an associated role")
) )
// DashboardErr represents a dashboard error.
type DashboardErr struct {
StatusCode int
Status string
Reason string
}
// Equal returns whether equal to another DashboardErr.
func (e DashboardErr) Equal(o DashboardErr) bool {
return o.StatusCode == e.StatusCode && o.Status == e.Status && o.Reason == e.Reason
}
// Error returns the error message.
func (e DashboardErr) Error() string {
if e.Reason != "" {
return e.Reason
}
return "Dashboard Error"
}
// Body returns the error's response body, if applicable.
func (e DashboardErr) Body() util.DynMap {
if e.Status == "" {
return nil
}
return util.DynMap{"status": e.Status, "message": e.Error()}
}

@ -4,115 +4,115 @@ import (
"errors" "errors"
"github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/apimachinery/errutil"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
) )
// Typed errors // Typed errors
var ( var (
ErrDashboardNotFound = DashboardErr{ ErrDashboardNotFound = dashboardaccess.DashboardErr{
Reason: "Dashboard not found", Reason: "Dashboard not found",
StatusCode: 404, StatusCode: 404,
Status: "not-found", Status: "not-found",
} }
ErrDashboardCorrupt = DashboardErr{ ErrDashboardCorrupt = dashboardaccess.DashboardErr{
Reason: "Dashboard data is missing or corrupt", Reason: "Dashboard data is missing or corrupt",
StatusCode: 500, StatusCode: 500,
Status: "not-found", Status: "not-found",
} }
ErrDashboardPanelNotFound = DashboardErr{ ErrDashboardPanelNotFound = dashboardaccess.DashboardErr{
Reason: "Dashboard panel not found", Reason: "Dashboard panel not found",
StatusCode: 404, StatusCode: 404,
Status: "not-found", Status: "not-found",
} }
ErrDashboardFolderNotFound = DashboardErr{ ErrDashboardFolderNotFound = dashboardaccess.DashboardErr{
Reason: "Folder not found", Reason: "Folder not found",
StatusCode: 404, StatusCode: 404,
} }
ErrDashboardWithSameUIDExists = DashboardErr{ ErrDashboardWithSameUIDExists = dashboardaccess.DashboardErr{
Reason: "A dashboard with the same uid already exists", Reason: "A dashboard with the same uid already exists",
StatusCode: 400, StatusCode: 400,
} }
ErrDashboardVersionMismatch = DashboardErr{ ErrDashboardVersionMismatch = dashboardaccess.DashboardErr{
Reason: "The dashboard has been changed by someone else", Reason: "The dashboard has been changed by someone else",
StatusCode: 412, StatusCode: 412,
Status: "version-mismatch", Status: "version-mismatch",
} }
ErrDashboardTitleEmpty = DashboardErr{ ErrDashboardTitleEmpty = dashboardaccess.DashboardErr{
Reason: "Dashboard title cannot be empty", Reason: "Dashboard title cannot be empty",
StatusCode: 400, StatusCode: 400,
Status: "empty-name", Status: "empty-name",
} }
ErrDashboardTitleTooLong = DashboardErr{ ErrDashboardTitleTooLong = dashboardaccess.DashboardErr{
Reason: "Dashboard title cannot contain more than 5 000 characters", Reason: "Dashboard title cannot contain more than 5 000 characters",
StatusCode: 400, StatusCode: 400,
Status: "title-too-long", Status: "title-too-long",
} }
ErrDashboardFolderCannotHaveParent = DashboardErr{ ErrDashboardFolderCannotHaveParent = dashboardaccess.DashboardErr{
Reason: "A Dashboard Folder cannot be added to another folder", Reason: "A Dashboard Folder cannot be added to another folder",
StatusCode: 400, StatusCode: 400,
} }
ErrDashboardsWithSameSlugExists = DashboardErr{ ErrDashboardsWithSameSlugExists = dashboardaccess.DashboardErr{
Reason: "Multiple dashboards with the same slug exists", Reason: "Multiple dashboards with the same slug exists",
StatusCode: 412, StatusCode: 412,
} }
ErrDashboardTypeMismatch = DashboardErr{ ErrDashboardTypeMismatch = dashboardaccess.DashboardErr{
Reason: "Dashboard cannot be changed to a folder", Reason: "Dashboard cannot be changed to a folder",
StatusCode: 400, StatusCode: 400,
} }
ErrDashboardFolderNameExists = DashboardErr{ ErrDashboardFolderNameExists = dashboardaccess.DashboardErr{
Reason: "A folder with that name already exists", Reason: "A folder with that name already exists",
StatusCode: 400, StatusCode: 400,
} }
ErrDashboardUpdateAccessDenied = DashboardErr{ ErrDashboardUpdateAccessDenied = dashboardaccess.DashboardErr{
Reason: "Access denied to save dashboard", Reason: "Access denied to save dashboard",
StatusCode: 403, StatusCode: 403,
} }
ErrDashboardInvalidUid = DashboardErr{ ErrDashboardInvalidUid = dashboardaccess.DashboardErr{
Reason: "uid contains illegal characters", Reason: "uid contains illegal characters",
StatusCode: 400, StatusCode: 400,
} }
ErrDashboardUidTooLong = DashboardErr{ ErrDashboardUidTooLong = dashboardaccess.DashboardErr{
Reason: "uid too long, max 40 characters", Reason: "uid too long, max 40 characters",
StatusCode: 400, StatusCode: 400,
} }
ErrDashboardMessageTooLong = DashboardErr{ ErrDashboardMessageTooLong = dashboardaccess.DashboardErr{
Reason: "message too long, max 500 characters", Reason: "message too long, max 500 characters",
StatusCode: 400, StatusCode: 400,
} }
ErrDashboardCannotSaveProvisionedDashboard = DashboardErr{ ErrDashboardCannotSaveProvisionedDashboard = dashboardaccess.DashboardErr{
Reason: "Cannot save provisioned dashboard", Reason: "Cannot save provisioned dashboard",
StatusCode: 400, StatusCode: 400,
} }
ErrDashboardRefreshIntervalTooShort = DashboardErr{ ErrDashboardRefreshIntervalTooShort = dashboardaccess.DashboardErr{
Reason: "Dashboard refresh interval is too low", Reason: "Dashboard refresh interval is too low",
StatusCode: 400, StatusCode: 400,
} }
ErrDashboardCannotDeleteProvisionedDashboard = DashboardErr{ ErrDashboardCannotDeleteProvisionedDashboard = dashboardaccess.DashboardErr{
Reason: "provisioned dashboard cannot be deleted", Reason: "provisioned dashboard cannot be deleted",
StatusCode: 400, StatusCode: 400,
} }
ErrDashboardIdentifierNotSet = DashboardErr{ ErrDashboardIdentifierNotSet = dashboardaccess.DashboardErr{
Reason: "Unique identifier needed to be able to get a dashboard", Reason: "Unique identifier needed to be able to get a dashboard",
StatusCode: 400, StatusCode: 400,
} }
ErrDashboardIdentifierInvalid = DashboardErr{ ErrDashboardIdentifierInvalid = dashboardaccess.DashboardErr{
Reason: "Dashboard ID not a number", Reason: "Dashboard ID not a number",
StatusCode: 400, StatusCode: 400,
} }
ErrDashboardPanelIdentifierInvalid = DashboardErr{ ErrDashboardPanelIdentifierInvalid = dashboardaccess.DashboardErr{
Reason: "Dashboard panel ID not a number", Reason: "Dashboard panel ID not a number",
StatusCode: 400, StatusCode: 400,
} }
ErrDashboardOrPanelIdentifierNotSet = DashboardErr{ ErrDashboardOrPanelIdentifierNotSet = dashboardaccess.DashboardErr{
Reason: "Unique identifier needed to be able to get a dashboard panel", Reason: "Unique identifier needed to be able to get a dashboard panel",
StatusCode: 400, StatusCode: 400,
} }
ErrProvisionedDashboardNotFound = DashboardErr{ ErrProvisionedDashboardNotFound = dashboardaccess.DashboardErr{
Reason: "Dashboard is not provisioned", Reason: "Dashboard is not provisioned",
StatusCode: 404, StatusCode: 404,
Status: "not-found", Status: "not-found",
} }
ErrFolderRestoreNotFound = DashboardErr{ ErrFolderRestoreNotFound = dashboardaccess.DashboardErr{
Reason: "Restoring folder not found", Reason: "Restoring folder not found",
StatusCode: 400, StatusCode: 400,
Status: "bad-request", Status: "bad-request",
@ -130,35 +130,6 @@ var (
ErrFolderCreationAccessDenied = errutil.Forbidden("folders.forbiddenCreation", errutil.WithPublicMessage("not enough permissions to create a folder in the selected location")) ErrFolderCreationAccessDenied = errutil.Forbidden("folders.forbiddenCreation", errutil.WithPublicMessage("not enough permissions to create a folder in the selected location"))
) )
// DashboardErr represents a dashboard error.
type DashboardErr struct {
StatusCode int
Status string
Reason string
}
// Equal returns whether equal to another DashboardErr.
func (e DashboardErr) Equal(o DashboardErr) bool {
return o.StatusCode == e.StatusCode && o.Status == e.Status && o.Reason == e.Reason
}
// Error returns the error message.
func (e DashboardErr) Error() string {
if e.Reason != "" {
return e.Reason
}
return "Dashboard Error"
}
// Body returns the error's response body, if applicable.
func (e DashboardErr) Body() util.DynMap {
if e.Status == "" {
return nil
}
return util.DynMap{"status": e.Status, "message": e.Error()}
}
type UpdatePluginDashboardError struct { type UpdatePluginDashboardError struct {
PluginId string PluginId string
} }

@ -19,6 +19,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/annotations"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/services/publicdashboards" "github.com/grafana/grafana/pkg/services/publicdashboards"
@ -143,7 +144,7 @@ func (pd *PublicDashboardServiceImpl) FindDashboard(ctx context.Context, orgId i
return pd.dashboardService.GetDashboard(ctx, &dashboards.GetDashboardQuery{UID: dashboardUid, OrgID: orgId}) return pd.dashboardService.GetDashboard(ctx, &dashboards.GetDashboardQuery{UID: dashboardUid, OrgID: orgId})
}) })
if err != nil { if err != nil {
var dashboardErr dashboards.DashboardErr var dashboardErr dashboardaccess.DashboardErr
if ok := errors.As(err, &dashboardErr); ok { if ok := errors.As(err, &dashboardErr); ok {
if dashboardErr.StatusCode == 404 { if dashboardErr.StatusCode == 404 {
return nil, ErrDashboardNotFound.Errorf("FindDashboard: dashboard not found by orgId: %d and dashboardUid: %s", orgId, dashboardUid) return nil, ErrDashboardNotFound.Errorf("FindDashboard: dashboard not found by orgId: %d and dashboardUid: %s", orgId, dashboardUid)

Loading…
Cancel
Save