mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 18:30:41 -06:00
Snapshots: Viewers can not create a Snapshot (#84952)
This commit is contained in:
parent
629a8808e0
commit
c57c033522
@ -11,6 +11,7 @@ import (
|
|||||||
dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1"
|
dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1"
|
||||||
"github.com/grafana/grafana/pkg/infra/appcontext"
|
"github.com/grafana/grafana/pkg/infra/appcontext"
|
||||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||||
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||||
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"
|
||||||
@ -68,12 +69,27 @@ func (hs *HTTPServer) GetSharingOptions(c *contextmodel.ReqContext) {
|
|||||||
// 403: forbiddenError
|
// 403: forbiddenError
|
||||||
// 500: internalServerError
|
// 500: internalServerError
|
||||||
func (hs *HTTPServer) CreateDashboardSnapshot(c *contextmodel.ReqContext) {
|
func (hs *HTTPServer) CreateDashboardSnapshot(c *contextmodel.ReqContext) {
|
||||||
|
cmd := dashboardsnapshots.CreateDashboardSnapshotCommand{}
|
||||||
|
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||||
|
c.JsonApiErr(http.StatusBadRequest, "bad request data", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not check permissions when the instance snapshot public mode is enabled
|
||||||
|
if !hs.Cfg.SnapshotPublicMode {
|
||||||
|
evaluator := ac.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(cmd.Dashboard.GetNestedString("uid")))
|
||||||
|
if canSave, err := hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator); err != nil || !canSave {
|
||||||
|
c.JsonApiErr(http.StatusForbidden, "forbidden", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dashboardsnapshots.CreateDashboardSnapshot(c, dashboardsnapshot.SnapshotSharingOptions{
|
dashboardsnapshots.CreateDashboardSnapshot(c, dashboardsnapshot.SnapshotSharingOptions{
|
||||||
SnapshotsEnabled: hs.Cfg.SnapshotEnabled,
|
SnapshotsEnabled: hs.Cfg.SnapshotEnabled,
|
||||||
ExternalEnabled: hs.Cfg.ExternalEnabled,
|
ExternalEnabled: hs.Cfg.ExternalEnabled,
|
||||||
ExternalSnapshotName: hs.Cfg.ExternalSnapshotName,
|
ExternalSnapshotName: hs.Cfg.ExternalSnapshotName,
|
||||||
ExternalSnapshotURL: hs.Cfg.ExternalSnapshotUrl,
|
ExternalSnapshotURL: hs.Cfg.ExternalSnapshotUrl,
|
||||||
}, hs.dashboardsnapshotsService)
|
}, cmd, hs.dashboardsnapshotsService)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /api/snapshots/:key
|
// GET /api/snapshots/:key
|
||||||
|
@ -269,6 +269,13 @@ func (b *SnapshotsAPIBuilder) GetAPIRoutes() *builder.APIRoutes {
|
|||||||
fmt.Sprintf("user orgId does not match namespace (%d != %d)", info.OrgID, user.OrgID), nil)
|
fmt.Sprintf("user orgId does not match namespace (%d != %d)", info.OrgID, user.OrgID), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd := dashboardsnapshots.CreateDashboardSnapshotCommand{}
|
||||||
|
if err := web.Bind(wrap.Req, &cmd); err != nil {
|
||||||
|
wrap.JsonApiErr(http.StatusBadRequest, "bad request data", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
opts, err := b.options(info.Value)
|
opts, err := b.options(info.Value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
wrap.JsonApiErr(http.StatusBadRequest, "error getting options", err)
|
wrap.JsonApiErr(http.StatusBadRequest, "error getting options", err)
|
||||||
@ -276,7 +283,7 @@ func (b *SnapshotsAPIBuilder) GetAPIRoutes() *builder.APIRoutes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use the existing snapshot service
|
// Use the existing snapshot service
|
||||||
dashboardsnapshots.CreateDashboardSnapshot(wrap, opts.Spec, b.service)
|
dashboardsnapshots.CreateDashboardSnapshot(wrap, opts.Spec, cmd, b.service)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -17,7 +17,6 @@ import (
|
|||||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
"github.com/grafana/grafana/pkg/web"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:generate mockery --name Service --structname MockService --inpackage --filename service_mock.go
|
//go:generate mockery --name Service --structname MockService --inpackage --filename service_mock.go
|
||||||
@ -34,19 +33,14 @@ var client = &http.Client{
|
|||||||
Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
|
Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateDashboardSnapshot(c *contextmodel.ReqContext, cfg dashboardsnapshot.SnapshotSharingOptions, svc Service) {
|
func CreateDashboardSnapshot(c *contextmodel.ReqContext, cfg dashboardsnapshot.SnapshotSharingOptions, cmd CreateDashboardSnapshotCommand, svc Service) {
|
||||||
if !cfg.SnapshotsEnabled {
|
if !cfg.SnapshotsEnabled {
|
||||||
c.JsonApiErr(http.StatusForbidden, "Dashboard Snapshots are disabled", nil)
|
c.JsonApiErr(http.StatusForbidden, "Dashboard Snapshots are disabled", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := CreateDashboardSnapshotCommand{}
|
if cmd.DashboardCreateCommand.Name == "" {
|
||||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
cmd.DashboardCreateCommand.Name = "Unnamed snapshot"
|
||||||
c.JsonApiErr(http.StatusBadRequest, "bad request data", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if cmd.Name == "" {
|
|
||||||
cmd.Name = "Unnamed snapshot"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
userID, err := identity.UserIdentifier(c.SignedInUser.GetNamespacedID())
|
userID, err := identity.UserIdentifier(c.SignedInUser.GetNamespacedID())
|
||||||
@ -66,7 +60,7 @@ func CreateDashboardSnapshot(c *contextmodel.ReqContext, cfg dashboardsnapshot.S
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if cmd.External {
|
if cmd.DashboardCreateCommand.External {
|
||||||
if !cfg.ExternalEnabled {
|
if !cfg.ExternalEnabled {
|
||||||
c.JsonApiErr(http.StatusForbidden, "External dashboard creation is disabled", nil)
|
c.JsonApiErr(http.StatusForbidden, "External dashboard creation is disabled", nil)
|
||||||
return
|
return
|
||||||
@ -83,11 +77,11 @@ func CreateDashboardSnapshot(c *contextmodel.ReqContext, cfg dashboardsnapshot.S
|
|||||||
cmd.DeleteKey = resp.DeleteKey
|
cmd.DeleteKey = resp.DeleteKey
|
||||||
cmd.ExternalURL = resp.Url
|
cmd.ExternalURL = resp.Url
|
||||||
cmd.ExternalDeleteURL = resp.DeleteUrl
|
cmd.ExternalDeleteURL = resp.DeleteUrl
|
||||||
cmd.Dashboard = &common.Unstructured{}
|
cmd.DashboardCreateCommand.Dashboard = &common.Unstructured{}
|
||||||
|
|
||||||
metrics.MApiDashboardSnapshotExternal.Inc()
|
metrics.MApiDashboardSnapshotExternal.Inc()
|
||||||
} else {
|
} else {
|
||||||
cmd.Dashboard.SetNestedField(originalDashboardURL, "snapshot", "originalUrl")
|
cmd.DashboardCreateCommand.Dashboard.SetNestedField(originalDashboardURL, "snapshot", "originalUrl")
|
||||||
|
|
||||||
if cmd.Key == "" {
|
if cmd.Key == "" {
|
||||||
var err error
|
var err error
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Code generated by mockery v2.16.0. DO NOT EDIT.
|
// Code generated by mockery v2.32.0. DO NOT EDIT.
|
||||||
|
|
||||||
package dashboardsnapshots
|
package dashboardsnapshots
|
||||||
|
|
||||||
@ -18,6 +18,10 @@ func (_m *MockService) CreateDashboardSnapshot(_a0 context.Context, _a1 *CreateD
|
|||||||
ret := _m.Called(_a0, _a1)
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
var r0 *DashboardSnapshot
|
var r0 *DashboardSnapshot
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *CreateDashboardSnapshotCommand) (*DashboardSnapshot, error)); ok {
|
||||||
|
return rf(_a0, _a1)
|
||||||
|
}
|
||||||
if rf, ok := ret.Get(0).(func(context.Context, *CreateDashboardSnapshotCommand) *DashboardSnapshot); ok {
|
if rf, ok := ret.Get(0).(func(context.Context, *CreateDashboardSnapshotCommand) *DashboardSnapshot); ok {
|
||||||
r0 = rf(_a0, _a1)
|
r0 = rf(_a0, _a1)
|
||||||
} else {
|
} else {
|
||||||
@ -26,7 +30,6 @@ func (_m *MockService) CreateDashboardSnapshot(_a0 context.Context, _a1 *CreateD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(1).(func(context.Context, *CreateDashboardSnapshotCommand) error); ok {
|
if rf, ok := ret.Get(1).(func(context.Context, *CreateDashboardSnapshotCommand) error); ok {
|
||||||
r1 = rf(_a0, _a1)
|
r1 = rf(_a0, _a1)
|
||||||
} else {
|
} else {
|
||||||
@ -69,6 +72,10 @@ func (_m *MockService) GetDashboardSnapshot(_a0 context.Context, _a1 *GetDashboa
|
|||||||
ret := _m.Called(_a0, _a1)
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
var r0 *DashboardSnapshot
|
var r0 *DashboardSnapshot
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardSnapshotQuery) (*DashboardSnapshot, error)); ok {
|
||||||
|
return rf(_a0, _a1)
|
||||||
|
}
|
||||||
if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardSnapshotQuery) *DashboardSnapshot); ok {
|
if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardSnapshotQuery) *DashboardSnapshot); ok {
|
||||||
r0 = rf(_a0, _a1)
|
r0 = rf(_a0, _a1)
|
||||||
} else {
|
} else {
|
||||||
@ -77,7 +84,6 @@ func (_m *MockService) GetDashboardSnapshot(_a0 context.Context, _a1 *GetDashboa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(1).(func(context.Context, *GetDashboardSnapshotQuery) error); ok {
|
if rf, ok := ret.Get(1).(func(context.Context, *GetDashboardSnapshotQuery) error); ok {
|
||||||
r1 = rf(_a0, _a1)
|
r1 = rf(_a0, _a1)
|
||||||
} else {
|
} else {
|
||||||
@ -92,6 +98,10 @@ func (_m *MockService) SearchDashboardSnapshots(_a0 context.Context, _a1 *GetDas
|
|||||||
ret := _m.Called(_a0, _a1)
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
var r0 DashboardSnapshotsList
|
var r0 DashboardSnapshotsList
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardSnapshotsQuery) (DashboardSnapshotsList, error)); ok {
|
||||||
|
return rf(_a0, _a1)
|
||||||
|
}
|
||||||
if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardSnapshotsQuery) DashboardSnapshotsList); ok {
|
if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardSnapshotsQuery) DashboardSnapshotsList); ok {
|
||||||
r0 = rf(_a0, _a1)
|
r0 = rf(_a0, _a1)
|
||||||
} else {
|
} else {
|
||||||
@ -100,7 +110,6 @@ func (_m *MockService) SearchDashboardSnapshots(_a0 context.Context, _a1 *GetDas
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(1).(func(context.Context, *GetDashboardSnapshotsQuery) error); ok {
|
if rf, ok := ret.Get(1).(func(context.Context, *GetDashboardSnapshotsQuery) error); ok {
|
||||||
r1 = rf(_a0, _a1)
|
r1 = rf(_a0, _a1)
|
||||||
} else {
|
} else {
|
||||||
@ -110,13 +119,12 @@ func (_m *MockService) SearchDashboardSnapshots(_a0 context.Context, _a1 *GetDas
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockConstructorTestingTNewMockService interface {
|
// NewMockService creates a new instance of MockService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||||
|
// The first argument is typically a *testing.T value.
|
||||||
|
func NewMockService(t interface {
|
||||||
mock.TestingT
|
mock.TestingT
|
||||||
Cleanup(func())
|
Cleanup(func())
|
||||||
}
|
}) *MockService {
|
||||||
|
|
||||||
// NewMockService creates a new instance of MockService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
|
||||||
func NewMockService(t mockConstructorTestingTNewMockService) *MockService {
|
|
||||||
mock := &MockService{}
|
mock := &MockService{}
|
||||||
mock.Mock.Test(t)
|
mock.Mock.Test(t)
|
||||||
|
|
||||||
|
@ -46,12 +46,13 @@ export class ShareModal extends SceneObjectBase<ShareModalState> implements Moda
|
|||||||
const { dashboardRef, panelRef } = this.state;
|
const { dashboardRef, panelRef } = this.state;
|
||||||
|
|
||||||
const tabs: SceneShareTab[] = [new ShareLinkTab({ dashboardRef, panelRef, modalRef: this.getRef() })];
|
const tabs: SceneShareTab[] = [new ShareLinkTab({ dashboardRef, panelRef, modalRef: this.getRef() })];
|
||||||
|
const dashboard = getDashboardSceneFor(this);
|
||||||
|
|
||||||
if (!panelRef) {
|
if (!panelRef) {
|
||||||
tabs.push(new ShareExportTab({ dashboardRef, modalRef: this.getRef() }));
|
tabs.push(new ShareExportTab({ dashboardRef, modalRef: this.getRef() }));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contextSrv.isSignedIn && config.snapshotEnabled) {
|
if (contextSrv.isSignedIn && config.snapshotEnabled && dashboard.canEditDashboard()) {
|
||||||
tabs.push(new ShareSnapshotTab({ panelRef, dashboardRef, modalRef: this.getRef() }));
|
tabs.push(new ShareSnapshotTab({ panelRef, dashboardRef, modalRef: this.getRef() }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,11 +29,11 @@ export function addPanelShareTab(tab: ShareModalTabModel) {
|
|||||||
customPanelTabs.push(tab);
|
customPanelTabs.push(tab);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTabs(panel?: PanelModel, activeTab?: string) {
|
function getTabs(canEditDashboard: boolean, panel?: PanelModel, activeTab?: string) {
|
||||||
const linkLabel = t('share-modal.tab-title.link', 'Link');
|
const linkLabel = t('share-modal.tab-title.link', 'Link');
|
||||||
const tabs: ShareModalTabModel[] = [{ label: linkLabel, value: shareDashboardType.link, component: ShareLink }];
|
const tabs: ShareModalTabModel[] = [{ label: linkLabel, value: shareDashboardType.link, component: ShareLink }];
|
||||||
|
|
||||||
if (contextSrv.isSignedIn && config.snapshotEnabled) {
|
if (contextSrv.isSignedIn && config.snapshotEnabled && canEditDashboard) {
|
||||||
const snapshotLabel = t('share-modal.tab-title.snapshot', 'Snapshot');
|
const snapshotLabel = t('share-modal.tab-title.snapshot', 'Snapshot');
|
||||||
tabs.push({ label: snapshotLabel, value: shareDashboardType.snapshot, component: ShareSnapshot });
|
tabs.push({ label: snapshotLabel, value: shareDashboardType.snapshot, component: ShareSnapshot });
|
||||||
}
|
}
|
||||||
@ -86,7 +86,7 @@ interface State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getInitialState(props: Props): State {
|
function getInitialState(props: Props): State {
|
||||||
const { tabs, activeTab } = getTabs(props.panel, props.activeTab);
|
const { tabs, activeTab } = getTabs(props.dashboard.canEditDashboard(), props.panel, props.activeTab);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tabs,
|
tabs,
|
||||||
@ -116,7 +116,8 @@ class UnthemedShareModal extends React.Component<Props, State> {
|
|||||||
const { panel } = this.props;
|
const { panel } = this.props;
|
||||||
const { activeTab } = this.state;
|
const { activeTab } = this.state;
|
||||||
const title = panel ? t('share-modal.panel.title', 'Share Panel') : t('share-modal.dashboard.title', 'Share');
|
const title = panel ? t('share-modal.panel.title', 'Share Panel') : t('share-modal.dashboard.title', 'Share');
|
||||||
const tabs = getTabs(this.props.panel, this.state.activeTab).tabs;
|
const canEditDashboard = this.props.dashboard.canEditDashboard();
|
||||||
|
const tabs = getTabs(canEditDashboard, this.props.panel, this.state.activeTab).tabs;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalTabsHeader
|
<ModalTabsHeader
|
||||||
|
Loading…
Reference in New Issue
Block a user