From f69654fcd5dd20b264e38af988a4a69673de76bb Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Wed, 30 May 2018 13:13:29 +0200 Subject: [PATCH 01/30] Restrict Explore UI to Editor and Admin roles Access is restricted via not showing in the following places: * hide from sidemenu * hide from panel header menu * disable keybinding `x` Also adds a `roles` property to reactContainer routes that will be checked if `roles` is set, and on failure redirects to `/`. --- pkg/api/index.go | 2 +- public/app/core/services/keybindingSrv.ts | 33 ++++++++++--------- .../app/features/panel/metrics_panel_ctrl.ts | 4 ++- public/app/routes/ReactContainer.tsx | 20 +++++++++-- public/app/routes/routes.ts | 1 + 5 files changed, 41 insertions(+), 19 deletions(-) diff --git a/pkg/api/index.go b/pkg/api/index.go index f082f03b5f6..acf0c30c907 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -128,7 +128,7 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) { Children: dashboardChildNavs, }) - if setting.ExploreEnabled { + if setting.ExploreEnabled && (c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR) { data.NavTree = append(data.NavTree, &dtos.NavLink{ Text: "Explore", Id: "explore", diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index 25d00ab37f1..b1021c90adc 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -14,7 +14,7 @@ export class KeybindingSrv { timepickerOpen = false; /** @ngInject */ - constructor(private $rootScope, private $location, private datasourceSrv, private timeSrv) { + constructor(private $rootScope, private $location, private datasourceSrv, private timeSrv, private contextSrv) { // clear out all shortcuts on route change $rootScope.$on('$routeChangeSuccess', () => { Mousetrap.reset(); @@ -177,21 +177,24 @@ export class KeybindingSrv { } }); - this.bind('x', async () => { - if (dashboard.meta.focusPanelId) { - const panel = dashboard.getPanelById(dashboard.meta.focusPanelId); - const datasource = await this.datasourceSrv.get(panel.datasource); - if (datasource && datasource.supportsExplore) { - const range = this.timeSrv.timeRangeForUrl(); - const state = { - ...datasource.getExploreState(panel), - range, - }; - const exploreState = encodePathComponent(JSON.stringify(state)); - this.$location.url(`/explore/${exploreState}`); + // jump to explore if permissions allow + if (this.contextSrv.isEditor) { + this.bind('x', async () => { + if (dashboard.meta.focusPanelId) { + const panel = dashboard.getPanelById(dashboard.meta.focusPanelId); + const datasource = await this.datasourceSrv.get(panel.datasource); + if (datasource && datasource.supportsExplore) { + const range = this.timeSrv.timeRangeForUrl(); + const state = { + ...datasource.getExploreState(panel), + range, + }; + const exploreState = encodePathComponent(JSON.stringify(state)); + this.$location.url(`/explore/${exploreState}`); + } } - } - }); + }); + } // delete panel this.bind('p r', () => { diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 3c48119ba3a..cf1b2cd49bc 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -16,6 +16,7 @@ class MetricsPanelCtrl extends PanelCtrl { datasourceName: any; $q: any; $timeout: any; + contextSrv: any; datasourceSrv: any; timeSrv: any; templateSrv: any; @@ -37,6 +38,7 @@ class MetricsPanelCtrl extends PanelCtrl { // make metrics tab the default this.editorTabIndex = 1; this.$q = $injector.get('$q'); + this.contextSrv = $injector.get('contextSrv'); this.datasourceSrv = $injector.get('datasourceSrv'); this.timeSrv = $injector.get('timeSrv'); this.templateSrv = $injector.get('templateSrv'); @@ -312,7 +314,7 @@ class MetricsPanelCtrl extends PanelCtrl { getAdditionalMenuItems() { const items = []; - if (this.datasource && this.datasource.supportsExplore) { + if (this.contextSrv.isEditor && this.datasource && this.datasource.supportsExplore) { items.push({ text: 'Explore', click: 'ctrl.explore();', diff --git a/public/app/routes/ReactContainer.tsx b/public/app/routes/ReactContainer.tsx index db6938cc878..b161a5e7a87 100644 --- a/public/app/routes/ReactContainer.tsx +++ b/public/app/routes/ReactContainer.tsx @@ -6,6 +6,7 @@ import coreModule from 'app/core/core_module'; import { store } from 'app/stores/store'; import { BackendSrv } from 'app/core/services/backend_srv'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; +import { ContextSrv } from 'app/core/services/context_srv'; function WrapInProvider(store, Component, props) { return ( @@ -16,16 +17,31 @@ function WrapInProvider(store, Component, props) { } /** @ngInject */ -export function reactContainer($route, $location, backendSrv: BackendSrv, datasourceSrv: DatasourceSrv) { +export function reactContainer( + $route, + $location, + backendSrv: BackendSrv, + datasourceSrv: DatasourceSrv, + contextSrv: ContextSrv +) { return { restrict: 'E', template: '', link(scope, elem) { - let component = $route.current.locals.component; + // Check permissions for this component + const { roles } = $route.current.locals; + if (roles && roles.length) { + if (!roles.some(r => contextSrv.hasRole(r))) { + $location.url('/'); + } + } + + let { component } = $route.current.locals; // Dynamic imports return whole module, need to extract default export if (component.default) { component = component.default; } + const props = { backendSrv: backendSrv, datasourceSrv: datasourceSrv, diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index b10084d1941..568b3438b38 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -113,6 +113,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { .when('/explore/:initial?', { template: '', resolve: { + roles: () => ['Editor', 'Admin'], component: () => import(/* webpackChunkName: "explore" */ 'app/containers/Explore/Wrapper'), }, }) From 7224ca6c622547124fcab828919872fde93efca6 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Wed, 30 May 2018 13:24:09 +0200 Subject: [PATCH 02/30] Fix panel menu test --- public/app/features/panel/specs/metrics_panel_ctrl.jest.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/features/panel/specs/metrics_panel_ctrl.jest.ts b/public/app/features/panel/specs/metrics_panel_ctrl.jest.ts index f2e5199b57d..79564e2a123 100644 --- a/public/app/features/panel/specs/metrics_panel_ctrl.jest.ts +++ b/public/app/features/panel/specs/metrics_panel_ctrl.jest.ts @@ -24,8 +24,9 @@ describe('MetricsPanelCtrl', () => { }); }); - describe('and has datasource set that supports explore', () => { + describe('and has datasource set that supports explore and user has powers', () => { beforeEach(() => { + ctrl.contextSrv = { isEditor: true }; ctrl.datasource = { supportsExplore: true }; additionalItems = ctrl.getAdditionalMenuItems(); }); From 827fb7e8de3bf075a2af13f8f6940abbee5eb584 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Wed, 30 May 2018 15:24:47 +0200 Subject: [PATCH 03/30] Fix karma tests that rely on MetricsPanelCtrl --- public/app/features/panel/metrics_panel_ctrl.ts | 4 ++-- public/test/specs/helpers.ts | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index cf1b2cd49bc..cbda8c874db 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -1,9 +1,9 @@ -import config from 'app/core/config'; import $ from 'jquery'; import _ from 'lodash'; + +import config from 'app/core/config'; import kbn from 'app/core/utils/kbn'; import { PanelCtrl } from 'app/features/panel/panel_ctrl'; - import * as rangeUtil from 'app/core/utils/rangeutil'; import * as dateMath from 'app/core/utils/datemath'; import { encodePathComponent } from 'app/core/utils/location_util'; diff --git a/public/test/specs/helpers.ts b/public/test/specs/helpers.ts index 276d9867ec4..dd8bd39846e 100644 --- a/public/test/specs/helpers.ts +++ b/public/test/specs/helpers.ts @@ -11,6 +11,7 @@ export function ControllerTestContext() { this.$element = {}; this.$sanitize = {}; this.annotationsSrv = {}; + this.contextSrv = {}; this.timeSrv = new TimeSrvStub(); this.templateSrv = new TemplateSrvStub(); this.datasourceSrv = { @@ -27,6 +28,7 @@ export function ControllerTestContext() { this.providePhase = function(mocks) { return angularMocks.module(function($provide) { + $provide.value('contextSrv', self.contextSrv); $provide.value('datasourceSrv', self.datasourceSrv); $provide.value('annotationsSrv', self.annotationsSrv); $provide.value('timeSrv', self.timeSrv); From 47d388437740d930f3273f99338a2721ec8a9225 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 21 May 2018 09:03:32 +0200 Subject: [PATCH 04/30] provisioning: follow symlinked folders fixes #11958 --- .../provisioning/dashboards/file_reader.go | 5 +++ .../dashboards/file_reader_linux_test.go | 39 +++++++++++++++++++ .../testdata/test-dashboards/symlink | 1 + 3 files changed, 45 insertions(+) create mode 100644 pkg/services/provisioning/dashboards/file_reader_linux_test.go create mode 120000 pkg/services/provisioning/dashboards/testdata/test-dashboards/symlink diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index 93846f5c474..628c63de3a8 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -47,6 +47,11 @@ func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReade log.Error("Cannot read directory", "error", err) } + path, err := filepath.EvalSymlinks(path) + if err != nil { + log.Error("Failed to read content of symlinked path: %s", path) + } + absPath, err := filepath.Abs(path) if err != nil { log.Error("Could not create absolute path ", "path", path) diff --git a/pkg/services/provisioning/dashboards/file_reader_linux_test.go b/pkg/services/provisioning/dashboards/file_reader_linux_test.go new file mode 100644 index 00000000000..9d4cdae8609 --- /dev/null +++ b/pkg/services/provisioning/dashboards/file_reader_linux_test.go @@ -0,0 +1,39 @@ +// +build linux + +package dashboards + +import ( + "path/filepath" + "testing" + + "github.com/grafana/grafana/pkg/log" +) + +var ( + symlinkedFolder = "testdata/test-dashboards/symlink" +) + +func TestProvsionedSymlinkedFolder(t *testing.T) { + cfg := &DashboardsAsConfig{ + Name: "Default", + Type: "file", + OrgId: 1, + Folder: "", + Options: map[string]interface{}{"path": symlinkedFolder}, + } + + reader, err := NewDashboardFileReader(cfg, log.New("test-logger")) + if err != nil { + t.Error("expected err to be nil") + } + + want, err := filepath.Abs(containingId) + + if err != nil { + t.Errorf("expected err to be nill") + } + + if reader.Path != want { + t.Errorf("got %s want %s", reader.Path, want) + } +} diff --git a/pkg/services/provisioning/dashboards/testdata/test-dashboards/symlink b/pkg/services/provisioning/dashboards/testdata/test-dashboards/symlink new file mode 120000 index 00000000000..42e166e6959 --- /dev/null +++ b/pkg/services/provisioning/dashboards/testdata/test-dashboards/symlink @@ -0,0 +1 @@ +containing-id/ \ No newline at end of file From 2bd4c14e5f4d0a525dd7f7b692484f8fbb8fc9bc Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 31 May 2018 09:53:15 +0200 Subject: [PATCH 05/30] make path absolute before following symlink --- .../provisioning/dashboards/file_reader.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index 628c63de3a8..a1ba4dbf8e2 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -47,20 +47,21 @@ func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReade log.Error("Cannot read directory", "error", err) } - path, err := filepath.EvalSymlinks(path) + copy := path + path, err := filepath.Abs(path) + if err != nil { + log.Error("Could not create absolute path ", "path", path) + path = copy //if .Abs return an error we fallback to path + } + + path, err = filepath.EvalSymlinks(path) if err != nil { log.Error("Failed to read content of symlinked path: %s", path) } - absPath, err := filepath.Abs(path) - if err != nil { - log.Error("Could not create absolute path ", "path", path) - absPath = path //if .Abs return an error we fallback to path - } - return &fileReader{ Cfg: cfg, - Path: absPath, + Path: path, log: log, dashboardService: dashboards.NewProvisioningService(), }, nil From 0c45ee63a9bf360ded82e3a229fd0a142187c797 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Thu, 31 May 2018 11:26:24 +0200 Subject: [PATCH 06/30] Guard /explore by editor role on the backend --- pkg/api/api.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/api/api.go b/pkg/api/api.go index 01189f7a81e..c205e7d3e2f 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -77,6 +77,9 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/dashboards/", reqSignedIn, Index) r.Get("/dashboards/*", reqSignedIn, Index) + r.Get("/explore/", reqEditorRole, Index) + r.Get("/explore/*", reqEditorRole, Index) + r.Get("/playlists/", reqSignedIn, Index) r.Get("/playlists/*", reqSignedIn, Index) r.Get("/alerting/", reqSignedIn, Index) From 44f5b92fbcd77330f28e61bdcb84d0b9499b6b47 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 31 May 2018 11:38:29 +0200 Subject: [PATCH 07/30] provisioning: only provision if json file is newer then db --- pkg/services/provisioning/dashboards/file_reader.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index 93846f5c474..cd4598794bc 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -159,7 +159,7 @@ func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.Fil } provisionedData, alreadyProvisioned := provisionedDashboardRefs[path] - upToDate := alreadyProvisioned && provisionedData.Updated == resolvedFileInfo.ModTime().Unix() + upToDate := alreadyProvisioned && provisionedData.Updated >= resolvedFileInfo.ModTime().Unix() dash, err := fr.readDashboardFromFile(path, resolvedFileInfo.ModTime(), folderId) if err != nil { From c817aecd660ea563e7bdb8f723e8bb39e3ad3c53 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 31 May 2018 14:13:34 +0200 Subject: [PATCH 08/30] provisioning: only update dashboard if hash of json changed --- .../dashboards/bulk-testing/bulkdash.jsonnet | 2 +- devenv/setup.sh | 6 +-- pkg/models/dashboards.go | 1 + .../provisioning/dashboards/file_reader.go | 45 ++++++++++++++++--- .../sqlstore/migrations/dashboard_mig.go | 4 ++ pkg/util/md5.go | 26 +++++++++++ pkg/util/md5_test.go | 17 +++++++ 7 files changed, 90 insertions(+), 11 deletions(-) create mode 100644 pkg/util/md5.go create mode 100644 pkg/util/md5_test.go diff --git a/devenv/dashboards/bulk-testing/bulkdash.jsonnet b/devenv/dashboards/bulk-testing/bulkdash.jsonnet index 17b3f8983af..4c82fd36f69 100644 --- a/devenv/dashboards/bulk-testing/bulkdash.jsonnet +++ b/devenv/dashboards/bulk-testing/bulkdash.jsonnet @@ -1137,4 +1137,4 @@ "title": "Big Dashboard", "uid": "000000003", "version": 16 -} \ No newline at end of file +} diff --git a/devenv/setup.sh b/devenv/setup.sh index d6f8f969e75..0a8958131fb 100755 --- a/devenv/setup.sh +++ b/devenv/setup.sh @@ -5,10 +5,10 @@ bulkDashboard() { requiresJsonnet COUNTER=0 - MAX=400 + MAX=4 while [ $COUNTER -lt $MAX ]; do jsonnet -o "dashboards/bulk-testing/dashboard${COUNTER}.json" -e "local bulkDash = import 'dashboards/bulk-testing/bulkdash.jsonnet'; bulkDash + { uid: 'uid-${COUNTER}', title: 'title-${COUNTER}' }" - let COUNTER=COUNTER+1 + let COUNTER=COUNTER+1 done ln -s -f -r ./dashboards/bulk-testing/bulk-dashboards.yaml ../conf/provisioning/dashboards/custom.yaml @@ -58,4 +58,4 @@ main() { fi } -main "$@" \ No newline at end of file +main "$@" diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index eb44c1bc582..4b84d840113 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -254,6 +254,7 @@ type DashboardProvisioning struct { DashboardId int64 Name string ExternalId string + CheckSum string Updated int64 } diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index cd4598794bc..e1cb92abc83 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -4,12 +4,14 @@ import ( "context" "errors" "fmt" + "io/ioutil" "os" "path/filepath" "strings" "time" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/bus" @@ -161,13 +163,18 @@ func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.Fil provisionedData, alreadyProvisioned := provisionedDashboardRefs[path] upToDate := alreadyProvisioned && provisionedData.Updated >= resolvedFileInfo.ModTime().Unix() - dash, err := fr.readDashboardFromFile(path, resolvedFileInfo.ModTime(), folderId) + jsonFile, err := fr.readDashboardFromFile(path, resolvedFileInfo.ModTime(), folderId) if err != nil { fr.log.Error("failed to load dashboard from ", "file", path, "error", err) return provisioningMetadata, nil } + if provisionedData != nil && jsonFile.checkSum == provisionedData.CheckSum { + upToDate = true + } + // keeps track of what uid's and title's we have already provisioned + dash := jsonFile.dashboard provisioningMetadata.uid = dash.Dashboard.Uid provisioningMetadata.title = dash.Dashboard.Title @@ -185,7 +192,13 @@ func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.Fil } fr.log.Debug("saving new dashboard", "file", path) - dp := &models.DashboardProvisioning{ExternalId: path, Name: fr.Cfg.Name, Updated: resolvedFileInfo.ModTime().Unix()} + dp := &models.DashboardProvisioning{ + ExternalId: path, + Name: fr.Cfg.Name, + Updated: resolvedFileInfo.ModTime().Unix(), + CheckSum: jsonFile.checkSum, + } + _, err = fr.dashboardService.SaveProvisionedDashboard(dash, dp) return provisioningMetadata, err } @@ -283,14 +296,30 @@ func validateWalkablePath(fileInfo os.FileInfo) (bool, error) { return true, nil } -func (fr *fileReader) readDashboardFromFile(path string, lastModified time.Time, folderId int64) (*dashboards.SaveDashboardDTO, error) { +type dashboardJsonFile struct { + dashboard *dashboards.SaveDashboardDTO + checkSum string + lastModified time.Time +} + +func (fr *fileReader) readDashboardFromFile(path string, lastModified time.Time, folderId int64) (*dashboardJsonFile, error) { reader, err := os.Open(path) if err != nil { return nil, err } defer reader.Close() - data, err := simplejson.NewFromReader(reader) + all, err := ioutil.ReadAll(reader) + if err != nil { + return nil, err + } + + checkSum, err := util.Md5SumString(string(all)) + if err != nil { + return nil, err + } + + data, err := simplejson.NewJson(all) if err != nil { return nil, err } @@ -300,7 +329,11 @@ func (fr *fileReader) readDashboardFromFile(path string, lastModified time.Time, return nil, err } - return dash, nil + return &dashboardJsonFile{ + dashboard: dash, + checkSum: checkSum, + lastModified: lastModified, + }, nil } type provisioningMetadata struct { @@ -328,7 +361,6 @@ func (checker provisioningSanityChecker) track(pm provisioningMetadata) { if len(pm.title) > 0 { checker.titleUsage[pm.title] += 1 } - } func (checker provisioningSanityChecker) logWarnings(log log.Logger) { @@ -343,5 +375,4 @@ func (checker provisioningSanityChecker) logWarnings(log log.Logger) { log.Error("the same 'title' is used more than once", "title", title, "provider", checker.provisioningProvider) } } - } diff --git a/pkg/services/sqlstore/migrations/dashboard_mig.go b/pkg/services/sqlstore/migrations/dashboard_mig.go index 170498c4bd9..b770afb1b4e 100644 --- a/pkg/services/sqlstore/migrations/dashboard_mig.go +++ b/pkg/services/sqlstore/migrations/dashboard_mig.go @@ -211,4 +211,8 @@ func addDashboardMigration(mg *Migrator) { "name": "name", "external_id": "external_id", }) + + mg.AddMigration("Add check_sum column", NewAddColumnMigration(dashboardExtrasTableV2, &Column{ + Name: "check_sum", Type: DB_NVarchar, Length: 32, Nullable: true, + })) } diff --git a/pkg/util/md5.go b/pkg/util/md5.go new file mode 100644 index 00000000000..2473a1a406c --- /dev/null +++ b/pkg/util/md5.go @@ -0,0 +1,26 @@ +package util + +import ( + "crypto/md5" + "encoding/hex" + "io" + "strings" +) + +// Md5Sum calculates the md5sum of a stream +func Md5Sum(reader io.Reader) (string, error) { + var returnMD5String string + hash := md5.New() + if _, err := io.Copy(hash, reader); err != nil { + return returnMD5String, err + } + hashInBytes := hash.Sum(nil)[:16] + returnMD5String = hex.EncodeToString(hashInBytes) + return returnMD5String, nil +} + +// Md5Sum calculates the md5sum of a string +func Md5SumString(input string) (string, error) { + buffer := strings.NewReader(input) + return Md5Sum(buffer) +} diff --git a/pkg/util/md5_test.go b/pkg/util/md5_test.go new file mode 100644 index 00000000000..1338d42bb51 --- /dev/null +++ b/pkg/util/md5_test.go @@ -0,0 +1,17 @@ +package util + +import "testing" + +func TestMd5Sum(t *testing.T) { + input := "dont hash passwords with md5" + + have, err := Md5SumString(input) + if err != nil { + t.Fatal("expected err to be nil") + } + + want := "2d6a56c82d09d374643b926d3417afba" + if have != want { + t.Fatalf("expected: %s got: %s", want, have) + } +} From 333af6fd9b5a01f5bda81928f79a7bb0779df245 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 31 May 2018 19:35:46 +0200 Subject: [PATCH 09/30] provisioning: makes the interval for polling for changes configurable --- devenv/setup.sh | 2 +- pkg/services/provisioning/dashboards/config_reader.go | 4 ++++ pkg/services/provisioning/dashboards/config_reader_test.go | 2 ++ pkg/services/provisioning/dashboards/file_reader.go | 4 +--- .../test-configs/dashboards-from-disk/dev-dashboards.yaml | 1 + .../testdata/test-configs/version-0/version-0.yaml | 1 + pkg/services/provisioning/dashboards/types.go | 5 +++++ 7 files changed, 15 insertions(+), 4 deletions(-) diff --git a/devenv/setup.sh b/devenv/setup.sh index 0a8958131fb..c75973ae3ce 100755 --- a/devenv/setup.sh +++ b/devenv/setup.sh @@ -5,7 +5,7 @@ bulkDashboard() { requiresJsonnet COUNTER=0 - MAX=4 + MAX=400 while [ $COUNTER -lt $MAX ]; do jsonnet -o "dashboards/bulk-testing/dashboard${COUNTER}.json" -e "local bulkDash = import 'dashboards/bulk-testing/bulkdash.jsonnet'; bulkDash + { uid: 'uid-${COUNTER}', title: 'title-${COUNTER}' }" let COUNTER=COUNTER+1 diff --git a/pkg/services/provisioning/dashboards/config_reader.go b/pkg/services/provisioning/dashboards/config_reader.go index 4f9577f82db..f8b6070c704 100644 --- a/pkg/services/provisioning/dashboards/config_reader.go +++ b/pkg/services/provisioning/dashboards/config_reader.go @@ -81,6 +81,10 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) { if dashboards[i].OrgId == 0 { dashboards[i].OrgId = 1 } + + if dashboards[i].IntervalSeconds == 0 { + dashboards[i].IntervalSeconds = 3 + } } return dashboards, nil diff --git a/pkg/services/provisioning/dashboards/config_reader_test.go b/pkg/services/provisioning/dashboards/config_reader_test.go index 25732089dcd..b49cd258005 100644 --- a/pkg/services/provisioning/dashboards/config_reader_test.go +++ b/pkg/services/provisioning/dashboards/config_reader_test.go @@ -68,6 +68,7 @@ func validateDashboardAsConfig(cfg []*DashboardsAsConfig) { So(len(ds.Options), ShouldEqual, 1) So(ds.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards") So(ds.DisableDeletion, ShouldBeTrue) + So(ds.IntervalSeconds, ShouldEqual, 10) ds2 := cfg[1] So(ds2.Name, ShouldEqual, "default") @@ -78,4 +79,5 @@ func validateDashboardAsConfig(cfg []*DashboardsAsConfig) { So(len(ds2.Options), ShouldEqual, 1) So(ds2.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards") So(ds2.DisableDeletion, ShouldBeFalse) + So(ds2.IntervalSeconds, ShouldEqual, 3) } diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index e1cb92abc83..a25b0208ad3 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -21,8 +21,6 @@ import ( ) var ( - checkDiskForChangesInterval = time.Second * 3 - ErrFolderNameMissing = errors.New("Folder name missing") ) @@ -68,7 +66,7 @@ func (fr *fileReader) ReadAndListen(ctx context.Context) error { fr.log.Error("failed to search for dashboards", "error", err) } - ticker := time.NewTicker(checkDiskForChangesInterval) + ticker := time.NewTicker(time.Duration(int64(time.Second) * fr.Cfg.IntervalSeconds)) running := false diff --git a/pkg/services/provisioning/dashboards/testdata/test-configs/dashboards-from-disk/dev-dashboards.yaml b/pkg/services/provisioning/dashboards/testdata/test-configs/dashboards-from-disk/dev-dashboards.yaml index e9776d69010..5ea2a0a4f75 100644 --- a/pkg/services/provisioning/dashboards/testdata/test-configs/dashboards-from-disk/dev-dashboards.yaml +++ b/pkg/services/provisioning/dashboards/testdata/test-configs/dashboards-from-disk/dev-dashboards.yaml @@ -6,6 +6,7 @@ providers: folder: 'developers' editable: true disableDeletion: true + intervalSeconds: 10 type: file options: path: /var/lib/grafana/dashboards diff --git a/pkg/services/provisioning/dashboards/testdata/test-configs/version-0/version-0.yaml b/pkg/services/provisioning/dashboards/testdata/test-configs/version-0/version-0.yaml index 979e762d4d4..bdbb06079fd 100644 --- a/pkg/services/provisioning/dashboards/testdata/test-configs/version-0/version-0.yaml +++ b/pkg/services/provisioning/dashboards/testdata/test-configs/version-0/version-0.yaml @@ -3,6 +3,7 @@ folder: 'developers' editable: true disableDeletion: true + intervalSeconds: 10 type: file options: path: /var/lib/grafana/dashboards diff --git a/pkg/services/provisioning/dashboards/types.go b/pkg/services/provisioning/dashboards/types.go index 4a55351d3e4..424e5e35f4a 100644 --- a/pkg/services/provisioning/dashboards/types.go +++ b/pkg/services/provisioning/dashboards/types.go @@ -17,6 +17,7 @@ type DashboardsAsConfig struct { Editable bool Options map[string]interface{} DisableDeletion bool + IntervalSeconds int64 } type DashboardsAsConfigV0 struct { @@ -27,6 +28,7 @@ type DashboardsAsConfigV0 struct { Editable bool `json:"editable" yaml:"editable"` Options map[string]interface{} `json:"options" yaml:"options"` DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"` + IntervalSeconds int64 `json:"intervalSeconds" yaml:"intervalSeconds"` } type ConfigVersion struct { @@ -45,6 +47,7 @@ type DashboardProviderConfigs struct { Editable bool `json:"editable" yaml:"editable"` Options map[string]interface{} `json:"options" yaml:"options"` DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"` + IntervalSeconds int64 `json:"intervalSeconds" yaml:"intervalSeconds"` } func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *DashboardsAsConfig, folderId int64) (*dashboards.SaveDashboardDTO, error) { @@ -75,6 +78,7 @@ func mapV0ToDashboardAsConfig(v0 []*DashboardsAsConfigV0) []*DashboardsAsConfig Editable: v.Editable, Options: v.Options, DisableDeletion: v.DisableDeletion, + IntervalSeconds: v.IntervalSeconds, }) } @@ -93,6 +97,7 @@ func (dc *DashboardAsConfigV1) mapToDashboardAsConfig() []*DashboardsAsConfig { Editable: v.Editable, Options: v.Options, DisableDeletion: v.DisableDeletion, + IntervalSeconds: v.IntervalSeconds, }) } From 75ee1e920890e2b7568407b0034cbddc01ebdce3 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 4 Jun 2018 08:13:20 +0200 Subject: [PATCH 10/30] renames intervalSeconds to updateIntervalSeconds --- docs/sources/administration/provisioning.md | 1 + .../provisioning/dashboards/config_reader.go | 4 +- .../dashboards/config_reader_test.go | 12 +-- .../provisioning/dashboards/file_reader.go | 2 +- .../dashboards-from-disk/dev-dashboards.yaml | 2 +- .../test-configs/version-0/version-0.yaml | 2 +- pkg/services/provisioning/dashboards/types.go | 80 +++++++++---------- 7 files changed, 53 insertions(+), 50 deletions(-) diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index 79b47aee9f6..888a0777796 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -197,6 +197,7 @@ providers: folder: '' type: file disableDeletion: false + updateIntervalSeconds: 3 #how often Grafana will scan for changed dashboards options: path: /var/lib/grafana/dashboards ``` diff --git a/pkg/services/provisioning/dashboards/config_reader.go b/pkg/services/provisioning/dashboards/config_reader.go index f8b6070c704..7508550838f 100644 --- a/pkg/services/provisioning/dashboards/config_reader.go +++ b/pkg/services/provisioning/dashboards/config_reader.go @@ -82,8 +82,8 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) { dashboards[i].OrgId = 1 } - if dashboards[i].IntervalSeconds == 0 { - dashboards[i].IntervalSeconds = 3 + if dashboards[i].UpdateIntervalSeconds == 0 { + dashboards[i].UpdateIntervalSeconds = 3 } } diff --git a/pkg/services/provisioning/dashboards/config_reader_test.go b/pkg/services/provisioning/dashboards/config_reader_test.go index b49cd258005..df0d2ae038e 100644 --- a/pkg/services/provisioning/dashboards/config_reader_test.go +++ b/pkg/services/provisioning/dashboards/config_reader_test.go @@ -22,7 +22,7 @@ func TestDashboardsAsConfig(t *testing.T) { cfg, err := cfgProvider.readConfig() So(err, ShouldBeNil) - validateDashboardAsConfig(cfg) + validateDashboardAsConfig(t, cfg) }) Convey("Can read config file in version 0 format", func() { @@ -30,7 +30,7 @@ func TestDashboardsAsConfig(t *testing.T) { cfg, err := cfgProvider.readConfig() So(err, ShouldBeNil) - validateDashboardAsConfig(cfg) + validateDashboardAsConfig(t, cfg) }) Convey("Should skip invalid path", func() { @@ -56,7 +56,9 @@ func TestDashboardsAsConfig(t *testing.T) { }) }) } -func validateDashboardAsConfig(cfg []*DashboardsAsConfig) { +func validateDashboardAsConfig(t *testing.T, cfg []*DashboardsAsConfig) { + t.Helper() + So(len(cfg), ShouldEqual, 2) ds := cfg[0] @@ -68,7 +70,7 @@ func validateDashboardAsConfig(cfg []*DashboardsAsConfig) { So(len(ds.Options), ShouldEqual, 1) So(ds.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards") So(ds.DisableDeletion, ShouldBeTrue) - So(ds.IntervalSeconds, ShouldEqual, 10) + So(ds.UpdateIntervalSeconds, ShouldEqual, 10) ds2 := cfg[1] So(ds2.Name, ShouldEqual, "default") @@ -79,5 +81,5 @@ func validateDashboardAsConfig(cfg []*DashboardsAsConfig) { So(len(ds2.Options), ShouldEqual, 1) So(ds2.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards") So(ds2.DisableDeletion, ShouldBeFalse) - So(ds2.IntervalSeconds, ShouldEqual, 3) + So(ds2.UpdateIntervalSeconds, ShouldEqual, 3) } diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index a25b0208ad3..89416d2596c 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -66,7 +66,7 @@ func (fr *fileReader) ReadAndListen(ctx context.Context) error { fr.log.Error("failed to search for dashboards", "error", err) } - ticker := time.NewTicker(time.Duration(int64(time.Second) * fr.Cfg.IntervalSeconds)) + ticker := time.NewTicker(time.Duration(int64(time.Second) * fr.Cfg.UpdateIntervalSeconds)) running := false diff --git a/pkg/services/provisioning/dashboards/testdata/test-configs/dashboards-from-disk/dev-dashboards.yaml b/pkg/services/provisioning/dashboards/testdata/test-configs/dashboards-from-disk/dev-dashboards.yaml index 5ea2a0a4f75..e26c329f87c 100644 --- a/pkg/services/provisioning/dashboards/testdata/test-configs/dashboards-from-disk/dev-dashboards.yaml +++ b/pkg/services/provisioning/dashboards/testdata/test-configs/dashboards-from-disk/dev-dashboards.yaml @@ -6,7 +6,7 @@ providers: folder: 'developers' editable: true disableDeletion: true - intervalSeconds: 10 + updateIntervalSeconds: 10 type: file options: path: /var/lib/grafana/dashboards diff --git a/pkg/services/provisioning/dashboards/testdata/test-configs/version-0/version-0.yaml b/pkg/services/provisioning/dashboards/testdata/test-configs/version-0/version-0.yaml index bdbb06079fd..69a317fb396 100644 --- a/pkg/services/provisioning/dashboards/testdata/test-configs/version-0/version-0.yaml +++ b/pkg/services/provisioning/dashboards/testdata/test-configs/version-0/version-0.yaml @@ -3,7 +3,7 @@ folder: 'developers' editable: true disableDeletion: true - intervalSeconds: 10 + updateIntervalSeconds: 10 type: file options: path: /var/lib/grafana/dashboards diff --git a/pkg/services/provisioning/dashboards/types.go b/pkg/services/provisioning/dashboards/types.go index 424e5e35f4a..a658b816c7d 100644 --- a/pkg/services/provisioning/dashboards/types.go +++ b/pkg/services/provisioning/dashboards/types.go @@ -10,25 +10,25 @@ import ( ) type DashboardsAsConfig struct { - Name string - Type string - OrgId int64 - Folder string - Editable bool - Options map[string]interface{} - DisableDeletion bool - IntervalSeconds int64 + Name string + Type string + OrgId int64 + Folder string + Editable bool + Options map[string]interface{} + DisableDeletion bool + UpdateIntervalSeconds int64 } type DashboardsAsConfigV0 struct { - Name string `json:"name" yaml:"name"` - Type string `json:"type" yaml:"type"` - OrgId int64 `json:"org_id" yaml:"org_id"` - Folder string `json:"folder" yaml:"folder"` - Editable bool `json:"editable" yaml:"editable"` - Options map[string]interface{} `json:"options" yaml:"options"` - DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"` - IntervalSeconds int64 `json:"intervalSeconds" yaml:"intervalSeconds"` + Name string `json:"name" yaml:"name"` + Type string `json:"type" yaml:"type"` + OrgId int64 `json:"org_id" yaml:"org_id"` + Folder string `json:"folder" yaml:"folder"` + Editable bool `json:"editable" yaml:"editable"` + Options map[string]interface{} `json:"options" yaml:"options"` + DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"` + UpdateIntervalSeconds int64 `json:"updateIntervalSeconds" yaml:"updateIntervalSeconds"` } type ConfigVersion struct { @@ -40,14 +40,14 @@ type DashboardAsConfigV1 struct { } type DashboardProviderConfigs struct { - Name string `json:"name" yaml:"name"` - Type string `json:"type" yaml:"type"` - OrgId int64 `json:"orgId" yaml:"orgId"` - Folder string `json:"folder" yaml:"folder"` - Editable bool `json:"editable" yaml:"editable"` - Options map[string]interface{} `json:"options" yaml:"options"` - DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"` - IntervalSeconds int64 `json:"intervalSeconds" yaml:"intervalSeconds"` + Name string `json:"name" yaml:"name"` + Type string `json:"type" yaml:"type"` + OrgId int64 `json:"orgId" yaml:"orgId"` + Folder string `json:"folder" yaml:"folder"` + Editable bool `json:"editable" yaml:"editable"` + Options map[string]interface{} `json:"options" yaml:"options"` + DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"` + UpdateIntervalSeconds int64 `json:"updateIntervalSeconds" yaml:"updateIntervalSeconds"` } func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *DashboardsAsConfig, folderId int64) (*dashboards.SaveDashboardDTO, error) { @@ -71,14 +71,14 @@ func mapV0ToDashboardAsConfig(v0 []*DashboardsAsConfigV0) []*DashboardsAsConfig for _, v := range v0 { r = append(r, &DashboardsAsConfig{ - Name: v.Name, - Type: v.Type, - OrgId: v.OrgId, - Folder: v.Folder, - Editable: v.Editable, - Options: v.Options, - DisableDeletion: v.DisableDeletion, - IntervalSeconds: v.IntervalSeconds, + Name: v.Name, + Type: v.Type, + OrgId: v.OrgId, + Folder: v.Folder, + Editable: v.Editable, + Options: v.Options, + DisableDeletion: v.DisableDeletion, + UpdateIntervalSeconds: v.UpdateIntervalSeconds, }) } @@ -90,14 +90,14 @@ func (dc *DashboardAsConfigV1) mapToDashboardAsConfig() []*DashboardsAsConfig { for _, v := range dc.Providers { r = append(r, &DashboardsAsConfig{ - Name: v.Name, - Type: v.Type, - OrgId: v.OrgId, - Folder: v.Folder, - Editable: v.Editable, - Options: v.Options, - DisableDeletion: v.DisableDeletion, - IntervalSeconds: v.IntervalSeconds, + Name: v.Name, + Type: v.Type, + OrgId: v.OrgId, + Folder: v.Folder, + Editable: v.Editable, + Options: v.Options, + DisableDeletion: v.DisableDeletion, + UpdateIntervalSeconds: v.UpdateIntervalSeconds, }) } From 3f5078339c0193a416775e719fd5c8a0293229ab Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 4 Jun 2018 08:27:03 +0200 Subject: [PATCH 11/30] tests: uses different paths depending on os --- .../provisioning/dashboards/file_reader_test.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pkg/services/provisioning/dashboards/file_reader_test.go b/pkg/services/provisioning/dashboards/file_reader_test.go index 87e9ec6d226..bdc1e95aafe 100644 --- a/pkg/services/provisioning/dashboards/file_reader_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_test.go @@ -49,13 +49,16 @@ func TestCreatingNewDashboardFileReader(t *testing.T) { }) Convey("using full path", func() { - cfg.Options["folder"] = "/var/lib/grafana/dashboards" + fullPath := "/var/lib/grafana/dashboards" + if runtime.GOOS == "windows" { + fullPath = `c:\var\lib\grafana` + } + + cfg.Options["folder"] = fullPath reader, err := NewDashboardFileReader(cfg, log.New("test-logger")) So(err, ShouldBeNil) - if runtime.GOOS != "windows" { - So(reader.Path, ShouldEqual, "/var/lib/grafana/dashboards") - } + So(reader.Path, ShouldEqual, fullPath) So(filepath.IsAbs(reader.Path), ShouldBeTrue) }) From f606654c50239fbc4616bcdd50c0441dd810ed1f Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 4 Jun 2018 09:04:33 +0200 Subject: [PATCH 12/30] provisioning: adds fallback if evalsymlink/abs fails --- pkg/services/provisioning/dashboards/file_reader.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index a1ba4dbf8e2..8af23980531 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -51,7 +51,6 @@ func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReade path, err := filepath.Abs(path) if err != nil { log.Error("Could not create absolute path ", "path", path) - path = copy //if .Abs return an error we fallback to path } path, err = filepath.EvalSymlinks(path) @@ -59,6 +58,11 @@ func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReade log.Error("Failed to read content of symlinked path: %s", path) } + if path == "" { + path = copy + log.Info("falling back to original path due to EvalSymlink/Abs failure") + } + return &fileReader{ Cfg: cfg, Path: path, From feb5e20779379863687e624e7ddf52e1c503061d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Jun 2018 11:17:50 +0200 Subject: [PATCH 13/30] datasource: added option no-direct-access to ds-http-settings diretive, closes #12138 --- public/app/features/plugins/ds_edit_ctrl.ts | 4 ++++ public/app/features/plugins/partials/ds_http_settings.html | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/public/app/features/plugins/ds_edit_ctrl.ts b/public/app/features/plugins/ds_edit_ctrl.ts index b98f0f48910..f86cc694255 100644 --- a/public/app/features/plugins/ds_edit_ctrl.ts +++ b/public/app/features/plugins/ds_edit_ctrl.ts @@ -204,10 +204,14 @@ coreModule.directive('datasourceHttpSettings', function() { scope: { current: '=', suggestUrl: '@', + noDirectAccess: '@', }, templateUrl: 'public/app/features/plugins/partials/ds_http_settings.html', link: { pre: function($scope, elem, attrs) { + // do not show access option if direct access is disabled + $scope.showAccessOption = $scope.noDirectAccess !== 'true'; + $scope.getSuggestUrls = function() { return [$scope.suggestUrl]; }; diff --git a/public/app/features/plugins/partials/ds_http_settings.html b/public/app/features/plugins/partials/ds_http_settings.html index b9f5683129c..b35aab0c099 100644 --- a/public/app/features/plugins/partials/ds_http_settings.html +++ b/public/app/features/plugins/partials/ds_http_settings.html @@ -22,7 +22,7 @@ -
+
Access
From 13c6f37ea581db9ecb04c859618847425d7cba46 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 1 Jun 2018 13:39:44 +0300 Subject: [PATCH 14/30] alerting: show alerts for user with Viewer role changelog: add notes about closing #11167 remove changelog note reformat alert_test.go --- pkg/api/alerting.go | 2 +- pkg/services/sqlstore/alert.go | 2 +- pkg/services/sqlstore/alert_test.go | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 961fc11b2dc..60013fe2b10 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -79,7 +79,7 @@ func GetAlerts(c *m.ReqContext) Response { DashboardIds: dashboardIDs, Type: string(search.DashHitDB), FolderIds: folderIDs, - Permission: m.PERMISSION_EDIT, + Permission: m.PERMISSION_VIEW, } err := bus.Dispatch(&searchQuery) diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index 58ec7e2857a..531a70b2101 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -116,7 +116,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error { } if query.User.OrgRole != m.ROLE_ADMIN { - builder.writeDashboardPermissionFilter(query.User, m.PERMISSION_EDIT) + builder.writeDashboardPermissionFilter(query.User, m.PERMISSION_VIEW) } builder.Write(" ORDER BY name ASC") diff --git a/pkg/services/sqlstore/alert_test.go b/pkg/services/sqlstore/alert_test.go index be48c7b2f52..79fa99864e7 100644 --- a/pkg/services/sqlstore/alert_test.go +++ b/pkg/services/sqlstore/alert_test.go @@ -2,7 +2,6 @@ package sqlstore import ( "testing" - "time" "github.com/grafana/grafana/pkg/components/simplejson" @@ -110,11 +109,12 @@ func TestAlertingDataAccess(t *testing.T) { }) Convey("Viewer cannot read alerts", func() { - alertQuery := m.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_VIEWER}} + viewerUser := &m.SignedInUser{OrgRole: m.ROLE_VIEWER, OrgId: 1} + alertQuery := m.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: viewerUser} err2 := HandleAlertsQuery(&alertQuery) So(err2, ShouldBeNil) - So(alertQuery.Result, ShouldHaveLength, 0) + So(alertQuery.Result, ShouldHaveLength, 1) }) Convey("Alerts with same dashboard id and panel id should update", func() { From e562ae753b75210a56d98e3689179bebb318d0f7 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 4 Jun 2018 11:49:12 +0200 Subject: [PATCH 15/30] docs: docker secrets support. (#12141) Closes #12132 --- CHANGELOG.md | 1 + docs/sources/installation/docker.md | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76e538a8e32..ecbc99608c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ * **Singlestat**: Fix singlestat threshold tooltip [#11971](https://github.com/grafana/grafana/issues/11971) * **Dashboard**: Hide grid controls in fullscreen/low-activity views [#11771](https://github.com/grafana/grafana/issues/11771) * **Dashboard**: Validate uid when importing dashboards [#11515](https://github.com/grafana/grafana/issues/11515) +* **Docker**: Support for env variables ending with _FILE [grafana-docker #166](https://github.com/grafana/grafana-docker/pull/166), thx [@efrecon](https://github.com/efrecon) # 5.1.3 (2018-05-16) diff --git a/docs/sources/installation/docker.md b/docs/sources/installation/docker.md index e78796845c4..e7dee84b5f4 100644 --- a/docs/sources/installation/docker.md +++ b/docs/sources/installation/docker.md @@ -130,6 +130,18 @@ ID=$(id -u) # saves your user id in the ID variable docker run -d --user $ID --volume "$PWD/data:/var/lib/grafana" -p 3000:3000 grafana/grafana:5.1.0 ``` +## Reading secrets from files (support for Docker Secrets) + +It's possible to supply Grafana with configuration through files. This works well with [Docker Secrets](https://docs.docker.com/engine/swarm/secrets/) as the secrets by default gets mapped into `/run/secrets/` of the container. + +You can do this with any of the configuration options in conf/grafana.ini by setting `GF___FILE` to the path of the file holding the secret. + +Let's say you want to set the admin password this way. + +- Admin password secret: `/run/secrets/admin_password` +- Environment variable: `GF_SECURITY_ADMIN_PASSWORD_FILE=/run/secrets/admin_password` + + ## Migration from a previous version of the docker container to 5.1 or later The docker container for Grafana has seen a major rewrite for 5.1. From 7453df2662c569643e0d358c8e06ae99af89041e Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 4 Jun 2018 11:57:13 +0200 Subject: [PATCH 16/30] changelog: add notes about closing #11167 [skip ci] --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecbc99608c4..9eda912e86a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,7 +30,8 @@ * **Singlestat**: Fix singlestat threshold tooltip [#11971](https://github.com/grafana/grafana/issues/11971) * **Dashboard**: Hide grid controls in fullscreen/low-activity views [#11771](https://github.com/grafana/grafana/issues/11771) * **Dashboard**: Validate uid when importing dashboards [#11515](https://github.com/grafana/grafana/issues/11515) -* **Docker**: Support for env variables ending with _FILE [grafana-docker #166](https://github.com/grafana/grafana-docker/pull/166), thx [@efrecon](https://github.com/efrecon) +* **Docker**: Support for env variables ending with _FILE [grafana-docker #166](https://github.com/grafana/grafana-docker/pull/166), thx [@efrecon](https://github.com/efrecon) +* **Alert list panel**: Show alerts for user with viewer role [#11167](https://github.com/grafana/grafana/issues/11167) # 5.1.3 (2018-05-16) From 0d5579b4c04fa7c04c3ae59950f962775a3f0777 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 4 Jun 2018 14:31:43 +0200 Subject: [PATCH 17/30] docs: what's new in v5.2 --- docs/sources/guides/whats-new-in-v5-2.md | 70 ++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 docs/sources/guides/whats-new-in-v5-2.md diff --git a/docs/sources/guides/whats-new-in-v5-2.md b/docs/sources/guides/whats-new-in-v5-2.md new file mode 100644 index 00000000000..8cff353ff45 --- /dev/null +++ b/docs/sources/guides/whats-new-in-v5-2.md @@ -0,0 +1,70 @@ ++++ +title = "What's New in Grafana v5.2" +description = "Feature & improvement highlights for Grafana v5.2" +keywords = ["grafana", "new", "documentation", "5.2"] +type = "docs" +[menu.docs] +name = "Version 5.2" +identifier = "v5.2" +parent = "whatsnew" +weight = -8 ++++ + +# What's New in Grafana v5.2 + +Grafana v5.2 brings new features, many enhancements and bug fixes. This article will detail the major new features and enhancements. + +* [Elasticsearch alerting]({{< relref "#elasticsearch-alerting" >}}) it's finally here! +* [Cross platform build support]({{< relref "#cross-platform-build-support" >}}) enables native builds of Grafana for many more platforms! +* [Improved Docker image]({{< relref "#improved-docker-image" >}}) with support for docker secrets +* [Prometheus]({{< relref "#prometheus" >}}) with alignment enhancements +* [Alerting]({{< relref "#alerting" >}}) with alert notification channel type for Discord +* [Dashboards & Panels]({{< relref "#dashboards-panels" >}}) + +## Elasticsearch alerting + +{{< docs-imagebox img="/img/docs/v52/elasticsearch_alerting.png" max-width="800px" class="docs-image--right" >}} + +Grafana v5.2 ships with an updated Elasticsearch datasource with support for alerting. Alerting support for Elasticsearch has been one of +the most requested features by our community and now it's finally here. Please try it out and let us know what you think. + +
+ +## Cross platform build support + +Grafana v5.2 brings an improved build pipeline with cross platform support. This enables native builds of Grafana for ARMv7 (x32), ARM64 (x64), +MacOS/Darwin (x64) and Windows (x64) in both stable and nightly builds. + +We've been longing for native ARM build support for a long time. With the help from our amazing community this is now finally available. + +## Improved Docker image + +The Grafana docker image now includes support for Docker secrets which enables you to supply Grafana with configuration through files. More +information in the [Installing using Docker documentation](/installation/docker/#reading-secrets-from-files-support-for-docker-secrets). + +## Prometheus + +The Prometheus datasource now aligns the start/end of the query sent to Prometheus with the step, which ensures PromQL expressions with *rate* +functions get consistent results, and thus avoid graphs jumping around on reload. + +## Alerting + +By popular demand Grafana now includes support for an alert notification channel type for [Discord](https://discordapp.com/). + +## Dashboards & Panels + +### Modified time range and variables are no longer saved by default + +{{< docs-imagebox img="/img/docs/v52/dashboard_save_modal.png" max-width="800px" class="docs-image--right" >}} + +Starting from Grafana v5.2 a modified time range or variable are no longer saved by default. To save a modified +time range or variable you'll need to actively select that when saving a dashboard, see screenshot. +This should hopefully make it easier to have sane defaults of time and variables in dashboards and make it more explicit +when you actually want to overwrite those settings. + +
+ +## Changelog + +Checkout the [CHANGELOG.md](https://github.com/grafana/grafana/blob/master/CHANGELOG.md) file for a complete list +of new features, changes, and bug fixes. From 38906acda98a43302f3f688042dc40e757284495 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 4 Jun 2018 15:15:47 +0200 Subject: [PATCH 18/30] elasticsearch: sort bucket keys to fix issue wth response parser tests --- pkg/tsdb/elasticsearch/response_parser.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pkg/tsdb/elasticsearch/response_parser.go b/pkg/tsdb/elasticsearch/response_parser.go index 4a45d6271b9..7bdab60389c 100644 --- a/pkg/tsdb/elasticsearch/response_parser.go +++ b/pkg/tsdb/elasticsearch/response_parser.go @@ -113,15 +113,22 @@ func (rp *responseParser) processBuckets(aggs map[string]interface{}, target *Qu } } - for k, v := range esAgg.Get("buckets").MustMap() { - bucket := simplejson.NewFromAny(v) + buckets := esAgg.Get("buckets").MustMap() + bucketKeys := make([]string, 0) + for k := range buckets { + bucketKeys = append(bucketKeys, k) + } + sort.Strings(bucketKeys) + + for _, bucketKey := range bucketKeys { + bucket := simplejson.NewFromAny(buckets[bucketKey]) newProps := make(map[string]string, 0) for k, v := range props { newProps[k] = v } - newProps["filter"] = k + newProps["filter"] = bucketKey err = rp.processBuckets(bucket.MustMap(), target, series, table, newProps, depth+1) if err != nil { From c138ff2c903c4cb7b5844529dae70037e651a15e Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 4 Jun 2018 15:16:05 +0200 Subject: [PATCH 19/30] changelog: adds note about closing #11670 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eda912e86a..f11d06e990a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ * **Dashboard**: Validate uid when importing dashboards [#11515](https://github.com/grafana/grafana/issues/11515) * **Docker**: Support for env variables ending with _FILE [grafana-docker #166](https://github.com/grafana/grafana-docker/pull/166), thx [@efrecon](https://github.com/efrecon) * **Alert list panel**: Show alerts for user with viewer role [#11167](https://github.com/grafana/grafana/issues/11167) +* **Provisioning**: Verify checksum of dashboards before updating to reduce load on database [#11670](https://github.com/grafana/grafana/issues/11670) # 5.1.3 (2018-05-16) From d089b5e05dccfd60d49b802be3a28ec3530fb0e8 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 4 Jun 2018 15:20:26 +0200 Subject: [PATCH 20/30] provisioning: turn relative symlinked path into absolut paths --- pkg/services/provisioning/dashboards/file_reader.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index 8af23980531..3196c3a35af 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -48,16 +48,25 @@ func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReade } copy := path + + // get absolut path of config file path, err := filepath.Abs(path) if err != nil { log.Error("Could not create absolute path ", "path", path) } + // follow the symlink to get the real path path, err = filepath.EvalSymlinks(path) if err != nil { log.Error("Failed to read content of symlinked path: %s", path) } + // get the absolut path in case the symlink is relative + path, err = filepath.Abs(path) + if err != nil { + log.Error("Could not create absolute path ", "path", path) + } + if path == "" { path = copy log.Info("falling back to original path due to EvalSymlink/Abs failure") From cd4026da6b60967dee2c51d626715913d1fa9914 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 4 Jun 2018 15:38:37 +0200 Subject: [PATCH 21/30] Revert "provisioning: turn relative symlinked path into absolut paths" This reverts commit d089b5e05dccfd60d49b802be3a28ec3530fb0e8. --- pkg/services/provisioning/dashboards/file_reader.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index 3196c3a35af..8af23980531 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -48,25 +48,16 @@ func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReade } copy := path - - // get absolut path of config file path, err := filepath.Abs(path) if err != nil { log.Error("Could not create absolute path ", "path", path) } - // follow the symlink to get the real path path, err = filepath.EvalSymlinks(path) if err != nil { log.Error("Failed to read content of symlinked path: %s", path) } - // get the absolut path in case the symlink is relative - path, err = filepath.Abs(path) - if err != nil { - log.Error("Could not create absolute path ", "path", path) - } - if path == "" { path = copy log.Info("falling back to original path due to EvalSymlink/Abs failure") From 829af9425f4e1f6d0d3cea9f8d5fa78e46bc4a2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Jun 2018 15:45:29 +0200 Subject: [PATCH 22/30] revert: reverted singlestat panel position change PR #12004 --- public/sass/components/_panel_singlestat.scss | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/public/sass/components/_panel_singlestat.scss b/public/sass/components/_panel_singlestat.scss index af11de3b835..d680941bfb1 100644 --- a/public/sass/components/_panel_singlestat.scss +++ b/public/sass/components/_panel_singlestat.scss @@ -7,14 +7,13 @@ .singlestat-panel-value-container { line-height: 1; - position: absolute; + display: table-cell; + vertical-align: middle; + text-align: center; + position: relative; z-index: 1; font-size: 3em; - font-weight: bold; - margin: 0; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); + font-weight: $font-weight-semi-bold; } .singlestat-panel-prefix { From 574e92e1d8497f2be17d781b2eeb3e98867d2b39 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 4 Jun 2018 15:23:17 +0200 Subject: [PATCH 23/30] changelog: adds note about closing #11958 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f11d06e990a..22e2c29c91b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ * **Docker**: Support for env variables ending with _FILE [grafana-docker #166](https://github.com/grafana/grafana-docker/pull/166), thx [@efrecon](https://github.com/efrecon) * **Alert list panel**: Show alerts for user with viewer role [#11167](https://github.com/grafana/grafana/issues/11167) * **Provisioning**: Verify checksum of dashboards before updating to reduce load on database [#11670](https://github.com/grafana/grafana/issues/11670) +* **Provisioning**: Support symlinked files in dashboard provisioning config files [#11958](https://github.com/grafana/grafana/issues/11958) # 5.1.3 (2018-05-16) From cb6c6c817234b59cce137f071ea31ccc58f1896d Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Wed, 23 May 2018 11:34:22 +0200 Subject: [PATCH 24/30] change admin password after first login --- public/app/core/controllers/login_ctrl.ts | 66 +++++++++-- public/app/partials/login.html | 135 +++++++++++++--------- public/sass/components/_gf-form.scss | 4 + public/sass/pages/_login.scss | 38 ++++++ 4 files changed, 184 insertions(+), 59 deletions(-) diff --git a/public/app/core/controllers/login_ctrl.ts b/public/app/core/controllers/login_ctrl.ts index 313fc2efa1a..0a66f83d08a 100644 --- a/public/app/core/controllers/login_ctrl.ts +++ b/public/app/core/controllers/login_ctrl.ts @@ -11,10 +11,15 @@ export class LoginCtrl { password: '', }; + $scope.command = {}; + $scope.result = ''; + contextSrv.sidemenu = false; $scope.oauth = config.oauth; $scope.oauthEnabled = _.keys(config.oauth).length > 0; + $scope.ldapEnabled = config.ldapEnabled; + $scope.authProxyEnabled = config.authProxyEnabled; $scope.disableLoginForm = config.disableLoginForm; $scope.disableUserSignUp = config.disableUserSignUp; @@ -39,6 +44,43 @@ export class LoginCtrl { } }; + $scope.changeView = function() { + let loginView = document.querySelector('#login-view'); + let changePasswordView = document.querySelector('#change-password-view'); + + loginView.className += ' add'; + setTimeout(() => { + loginView.className += ' hidden'; + }, 250); + setTimeout(() => { + changePasswordView.classList.remove('hidden'); + }, 251); + setTimeout(() => { + changePasswordView.classList.remove('remove'); + }, 301); + + setTimeout(() => { + document.getElementById('newPassword').focus(); + }, 400); + }; + + $scope.changePassword = function() { + $scope.command.oldPassword = 'admin'; + + if ($scope.command.newPassword !== $scope.command.confirmNew) { + $scope.appEvent('alert-warning', ['New passwords do not match', '']); + return; + } + + backendSrv.put('/api/user/password', $scope.command).then(function() { + $scope.toGrafana(); + }); + }; + + $scope.skip = function() { + $scope.toGrafana(); + }; + $scope.loginModeChanged = function(newValue) { $scope.submitBtnText = newValue ? 'Log in' : 'Sign up'; }; @@ -65,18 +107,28 @@ export class LoginCtrl { } backendSrv.post('/login', $scope.formModel).then(function(result) { - var params = $location.search(); + $scope.result = result; - if (params.redirect && params.redirect[0] === '/') { - window.location.href = config.appSubUrl + params.redirect; - } else if (result.redirectUrl) { - window.location.href = result.redirectUrl; - } else { - window.location.href = config.appSubUrl + '/'; + if ($scope.formModel.password !== 'admin' || $scope.ldapEnabled || $scope.authProxyEnabled) { + $scope.toGrafana(); + return; } + $scope.changeView(); }); }; + $scope.toGrafana = function() { + var params = $location.search(); + + if (params.redirect && params.redirect[0] === '/') { + window.location.href = config.appSubUrl + params.redirect; + } else if ($scope.result.redirectUrl) { + window.location.href = $scope.result.redirectUrl; + } else { + window.location.href = config.appSubUrl + '/'; + } + }; + $scope.init(); } } diff --git a/public/app/partials/login.html b/public/app/partials/login.html index 8680924977f..8be9e777b9f 100644 --- a/public/app/partials/login.html +++ b/public/app/partials/login.html @@ -4,70 +4,101 @@ Grafana
-