Просмотр исходного кода

Merge branch 'master' into 10427_addpanel_filter

Marcus Efraimsson 7 лет назад
Родитель
Сommit
82054e1a3e
100 измененных файлов с 1883 добавлено и 998 удалено
  1. 2 2
      .bra.toml
  2. 20 0
      .circleci/config.yml
  3. 6 0
      CHANGELOG.md
  4. 1 0
      Gruntfile.js
  5. 22 9
      build.go
  6. 11 0
      codecov.yml
  7. 2 2
      docs/sources/features/datasources/elasticsearch.md
  8. 2 2
      docs/sources/features/datasources/influxdb.md
  9. 2 2
      docs/sources/http_api/data_source.md
  10. 1 0
      package.json
  11. 11 3
      pkg/api/api.go
  12. 2 5
      pkg/api/avatar/avatar.go
  13. 12 1
      pkg/api/dashboard.go
  14. 5 0
      pkg/api/dashboard_permission.go
  15. 1 1
      pkg/api/dashboard_permission_test.go
  16. 21 0
      pkg/api/dashboard_test.go
  17. 1 0
      pkg/api/dtos/dashboard.go
  18. 6 0
      pkg/api/folder_permission.go
  19. 1 1
      pkg/api/http_server.go
  20. 1 1
      pkg/cmd/grafana-cli/commands/install_command.go
  21. 1 1
      pkg/cmd/grafana-cli/commands/ls_command.go
  22. 1 2
      pkg/cmd/grafana-cli/commands/upgrade_all_command.go
  23. 1 1
      pkg/cmd/grafana-cli/services/services.go
  24. 2 2
      pkg/components/apikeygen/apikeygen.go
  25. 0 7
      pkg/components/dynmap/dynmap.go
  26. 4 4
      pkg/components/dynmap/dynmap_test.go
  27. 1 1
      pkg/components/imguploader/azureblobuploader.go
  28. 1 1
      pkg/components/null/float.go
  29. 1 4
      pkg/log/file.go
  30. 1 2
      pkg/login/ldap.go
  31. 1 5
      pkg/metrics/graphitebridge/graphite.go
  32. 26 18
      pkg/metrics/metrics.go
  33. 3 0
      pkg/models/dashboard_acl.go
  34. 32 20
      pkg/models/dashboards.go
  35. 1 1
      pkg/models/datasource_cache.go
  36. 2 2
      pkg/models/org_user.go
  37. 1 1
      pkg/plugins/datasource/wrapper/datasource_plugin_wrapper_test.go
  38. 1 1
      pkg/plugins/models.go
  39. 1 1
      pkg/plugins/update_checker.go
  40. 3 3
      pkg/services/alerting/conditions/evaluator.go
  41. 1 1
      pkg/services/alerting/conditions/query.go
  42. 1 1
      pkg/services/alerting/extractor.go
  43. 1 1
      pkg/services/alerting/notifiers/line.go
  44. 1 1
      pkg/services/alerting/notifiers/telegram.go
  45. 2 2
      pkg/services/alerting/rule.go
  46. 2 2
      pkg/services/alerting/scheduler.go
  47. 28 5
      pkg/services/dashboards/dashboard_service.go
  48. 114 2
      pkg/services/dashboards/dashboard_service_test.go
  49. 2 2
      pkg/services/dashboards/folder_service.go
  50. 11 0
      pkg/services/dashboards/folder_service_test.go
  51. 1 1
      pkg/services/guardian/guardian.go
  52. 617 664
      pkg/services/guardian/guardian_test.go
  53. 256 0
      pkg/services/guardian/guardian_util_test.go
  54. 1 2
      pkg/services/notifications/mailer.go
  55. 0 3
      pkg/services/provisioning/dashboards/types.go
  56. 1 5
      pkg/services/provisioning/provisioning.go
  57. 1 6
      pkg/services/sqlstore/alert_notification.go
  58. 2 5
      pkg/services/sqlstore/annotation.go
  59. 2 2
      pkg/services/sqlstore/apikey.go
  60. 21 18
      pkg/services/sqlstore/dashboard.go
  61. 3 4
      pkg/services/sqlstore/dashboard_acl.go
  62. 14 0
      pkg/services/sqlstore/dashboard_provisioning.go
  63. 17 0
      pkg/services/sqlstore/dashboard_provisioning_test.go
  64. 165 35
      pkg/services/sqlstore/dashboard_service_integration_test.go
  65. 1 1
      pkg/services/sqlstore/dashboard_snapshot.go
  66. 2 4
      pkg/services/sqlstore/migrator/dialect.go
  67. 1 2
      pkg/services/sqlstore/migrator/migrations.go
  68. 1 1
      pkg/services/sqlstore/migrator/types.go
  69. 1 1
      pkg/services/sqlstore/plugin_setting.go
  70. 4 4
      pkg/services/sqlstore/quota.go
  71. 1 4
      pkg/services/sqlstore/stats.go
  72. 1 5
      pkg/services/sqlstore/team.go
  73. 1 1
      pkg/services/sqlstore/temp_user.go
  74. 11 20
      pkg/services/sqlstore/user.go
  75. 1 1
      pkg/social/generic_oauth.go
  76. 2 2
      pkg/tsdb/cloudwatch/annotation_query.go
  77. 0 3
      pkg/tsdb/cloudwatch/cloudwatch.go
  78. 4 15
      pkg/tsdb/cloudwatch/metric_find_query.go
  79. 1 4
      pkg/tsdb/cloudwatch/metric_find_query_test.go
  80. 5 5
      pkg/tsdb/mssql/mssql.go
  81. 4 4
      pkg/tsdb/mysql/mysql.go
  82. 4 4
      pkg/tsdb/postgres/macros.go
  83. 10 10
      pkg/tsdb/postgres/macros_test.go
  84. 5 5
      pkg/tsdb/postgres/postgres.go
  85. 2 2
      pkg/tsdb/prometheus/prometheus.go
  86. 1 1
      pkg/tsdb/sql_engine.go
  87. 8 0
      pkg/tsdb/time_range.go
  88. 1 5
      pkg/util/shortid_generator.go
  89. 2 2
      public/app/core/components/Permissions/AddPermissions.tsx
  90. 7 4
      public/app/core/components/Permissions/DisabledPermissionsListItem.tsx
  91. 1 2
      public/app/core/components/Permissions/Permissions.tsx
  92. 2 2
      public/app/core/components/Permissions/PermissionsList.tsx
  93. 30 4
      public/app/core/components/Permissions/PermissionsListItem.tsx
  94. 1 1
      public/app/core/components/sidemenu/sidemenu.html
  95. 1 0
      public/app/features/dashboard/all.ts
  96. 186 0
      public/app/features/dashboard/change_tracker.ts
  97. 2 1
      public/app/features/dashboard/dashboard_model.ts
  98. 10 0
      public/app/features/dashboard/dashboard_srv.ts
  99. 14 10
      public/app/features/dashboard/folder_picker/folder_picker.ts
  100. 77 0
      public/app/features/dashboard/save_provisioned_modal.ts

+ 2 - 2
.bra.toml

@@ -1,6 +1,6 @@
 [run]
 init_cmds = [
-  ["go", "build", "-o", "./bin/grafana-server", "./pkg/cmd/grafana-server"],
+  ["go", "run", "build.go", "-dev", "build-server"],
 	["./bin/grafana-server", "cfg:app_mode=development"]
 ]
 watch_all = true
@@ -12,6 +12,6 @@ watch_dirs = [
 watch_exts = [".go", ".ini", ".toml"]
 build_delay = 1500
 cmds = [
-  ["go", "build", "-o", "./bin/grafana-server", "./pkg/cmd/grafana-server"],
+  ["go", "run", "build.go", "-dev", "build"],
 	["./bin/grafana-server", "cfg:app_mode=development"]
 ]

+ 20 - 0
.circleci/config.yml

@@ -1,6 +1,22 @@
 version: 2
 
 jobs:
+  codespell:
+    docker:
+      - image: circleci/python
+    steps:
+      - checkout
+      - run:
+          name: install codespell
+          command: 'sudo pip install codespell'
+      - run:
+          # Important: all words have to be in lowercase, and separated by "\n".
+          name: exclude known exceptions
+          command: 'echo -e "unknwon" > words_to_ignore.txt'
+      - run:
+          name: check documentation spelling errors
+          command: 'codespell -I ./words_to_ignore.txt docs/'
+
   test-frontend:
     docker:
       - image: circleci/node:6.11.4
@@ -103,6 +119,10 @@ workflows:
   version: 2
   test-and-build:
     jobs:
+      - codespell:
+          filters:
+            tags:
+              only: /.*/
       - build:
           filters:
             tags:

+ 6 - 0
CHANGELOG.md

@@ -17,6 +17,8 @@
 * **IE11**: IE 11 compatibility [#11165](https://github.com/grafana/grafana/issues/11165)
 * **Scrolling**: Better scrolling experience [#11053](https://github.com/grafana/grafana/issues/11053), [#11252](https://github.com/grafana/grafana/issues/11252), [#10836](https://github.com/grafana/grafana/issues/10836), [#11185](https://github.com/grafana/grafana/issues/11185), [#11168](https://github.com/grafana/grafana/issues/11168)
 * **Docker**: Improved docker image (breaking changes regarding file ownership) [grafana-docker #141](https://github.com/grafana/grafana-docker/issues/141), thx [@Spindel](https://github.com/Spindel), [@ChristianKniep](https://github.com/ChristianKniep), [@brancz](https://github.com/brancz) and [@jangaraj](https://github.com/jangaraj)
+* **Folders**: A folder admin cannot add user/team permissions for folder/its dashboards [#11173](https://github.com/grafana/grafana/issues/11173)
+* **Provisioning**: Improved workflow for provisioned dashboards [#10883](https://github.com/grafana/grafana/issues/10883)
 
 ### Minor
 
@@ -49,6 +51,10 @@
 * **Prometheus**: tooltip for legend format not showing properly [#11516](https://github.com/grafana/grafana/issues/11516), thx [@svenklemm](https://github.com/svenklemm) 
 * **Playlist**: Empty playlists cannot be deleted [#11133](https://github.com/grafana/grafana/issues/11133), thx [@kichristensen](https://github.com/kichristensen) 
 * **Switch Orgs**: Alphabetic order in Switch Organization modal [#11556](https://github.com/grafana/grafana/issues/11556)
+* **Postgres**: improve `$__timeFilter` macro [#11578](https://github.com/grafana/grafana/issues/11578), thx [@svenklemm](https://github.com/svenklemm)
+* **Permission list**: Improved ux [#10747](https://github.com/grafana/grafana/issues/10747)
+* **Dashboard**: Sizing and positioning of settings menu icons [#11572](https://github.com/grafana/grafana/pull/11572)
+* **Folders**: User with org viewer role should not be able to save/move dashboards in/to general folder [#11553](https://github.com/grafana/grafana/issues/11553)
 
 ### Tech
 * Migrated JavaScript files to TypeScript

+ 1 - 0
Gruntfile.js

@@ -22,6 +22,7 @@ module.exports = function (grunt) {
     }
   }
 
+  config.coverage = grunt.option('coverage');
   config.phjs = grunt.option('phjsToRelease');
   config.pkg.version = grunt.option('pkgVer') || config.pkg.version;
 

+ 22 - 9
build.go

@@ -41,6 +41,7 @@ var (
 	includeBuildNumber    bool     = true
 	buildNumber           int      = 0
 	binaries              []string = []string{"grafana-server", "grafana-cli"}
+	isDev                 bool     = false
 )
 
 const minGoVersion = 1.8
@@ -61,6 +62,7 @@ func main() {
 	flag.BoolVar(&race, "race", race, "Use race detector")
 	flag.BoolVar(&includeBuildNumber, "includeBuildNumber", includeBuildNumber, "IncludeBuildNumber in package name")
 	flag.IntVar(&buildNumber, "buildNumber", 0, "Build number from CI system")
+	flag.BoolVar(&isDev, "dev", isDev, "optimal for development, skips certain steps")
 	flag.Parse()
 
 	readVersionFromPackageJson()
@@ -394,7 +396,9 @@ func build(binaryName, pkg string, tags []string) {
 		binary += ".exe"
 	}
 
-	rmr(binary, binary+".md5")
+	if !isDev {
+		rmr(binary, binary+".md5")
+	}
 	args := []string{"build", "-ldflags", ldflags()}
 	if len(tags) > 0 {
 		args = append(args, "-tags", strings.Join(tags, ","))
@@ -405,16 +409,21 @@ func build(binaryName, pkg string, tags []string) {
 
 	args = append(args, "-o", binary)
 	args = append(args, pkg)
-	setBuildEnv()
 
-	runPrint("go", "version")
+	if !isDev {
+		setBuildEnv()
+		runPrint("go", "version")
+	}
+
 	runPrint("go", args...)
 
-	// Create an md5 checksum of the binary, to be included in the archive for
-	// automatic upgrades.
-	err := md5File(binary)
-	if err != nil {
-		log.Fatal(err)
+	if !isDev {
+		// Create an md5 checksum of the binary, to be included in the archive for
+		// automatic upgrades.
+		err := md5File(binary)
+		if err != nil {
+			log.Fatal(err)
+		}
 	}
 }
 
@@ -435,6 +444,10 @@ func rmr(paths ...string) {
 }
 
 func clean() {
+	if isDev {
+		return
+	}
+
 	rmr("dist")
 	rmr("tmp")
 	rmr(filepath.Join(os.Getenv("GOPATH"), fmt.Sprintf("pkg/%s_%s/github.com/grafana", goos, goarch)))
@@ -550,7 +563,7 @@ func shaFilesInDist() {
 			return nil
 		}
 
-		if strings.Contains(path, ".sha256") == false {
+		if !strings.Contains(path, ".sha256") {
 			err := shaFile(path)
 			if err != nil {
 				log.Printf("Failed to create sha file. error: %v\n", err)

+ 11 - 0
codecov.yml

@@ -0,0 +1,11 @@
+coverage:
+  precision: 2
+  round: down
+  range: "50...100"
+
+  status:
+    project: yes
+    patch: yes
+    changes: no
+
+comment: off

+ 2 - 2
docs/sources/features/datasources/elasticsearch.md

@@ -57,7 +57,7 @@ are supported.
 
 ### Min time interval
 A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example `1m` if your data is written every minute.
-This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formated as a
+This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formatted as a
 number followed by a valid time identifier, e.g. `1m` (1 minute) or `30s` (30 seconds). The following time identifiers are supported:
 
 Identifier | Description
@@ -172,4 +172,4 @@ datasources:
     jsonData:
       interval: Daily
       timeField: "@timestamp"
-```
+```

+ 2 - 2
docs/sources/features/datasources/influxdb.md

@@ -41,7 +41,7 @@ mode is also more secure as the username & password will never reach the browser
 
 ### Min time interval
 A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example `1m` if your data is written every minute.
-This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formated as a
+This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formatted as a
 number followed by a valid time identifier, e.g. `1m` (1 minute) or `30s` (30 seconds). The following time identifiers are supported:
 
 Identifier | Description
@@ -208,4 +208,4 @@ datasources:
     user: grafana
     password: grafana
     url: http://localhost:8086
-```
+```

+ 2 - 2
docs/sources/http_api/data_source.md

@@ -188,8 +188,8 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
     "defaultRegion": "us-west-1"
   },
   "secureJsonData": {
-    "accessKey": "Ol4pIDpeKSA6XikgOl4p",
-    "secretKey": "dGVzdCBrZXkgYmxlYXNlIGRvbid0IHN0ZWFs"
+    "accessKey": "Ol4pIDpeKSA6XikgOl4p", //should not be encoded
+    "secretKey": "dGVzdCBrZXkgYmxlYXNlIGRvbid0IHN0ZWFs" //should be Base-64 encoded
   }
 }
 ```

+ 1 - 0
package.json

@@ -102,6 +102,7 @@
     "watch": "webpack --progress --colors --watch --config scripts/webpack/webpack.dev.js",
     "build": "grunt build",
     "test": "grunt test",
+    "test:coverage": "grunt test --coverage=true",
     "lint": "tslint -c tslint.json --project tsconfig.json --type-check",
     "karma": "grunt karma:dev",
     "jest": "jest --notify --watch",

+ 11 - 3
pkg/api/api.go

@@ -149,8 +149,6 @@ func (hs *HTTPServer) registerRoutes() {
 
 		// team (admin permission required)
 		apiRoute.Group("/teams", func(teamsRoute RouteRegister) {
-			teamsRoute.Get("/:teamId", wrap(GetTeamByID))
-			teamsRoute.Get("/search", wrap(SearchTeams))
 			teamsRoute.Post("/", bind(m.CreateTeamCommand{}), wrap(CreateTeam))
 			teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), wrap(UpdateTeam))
 			teamsRoute.Delete("/:teamId", wrap(DeleteTeamByID))
@@ -159,6 +157,12 @@ func (hs *HTTPServer) registerRoutes() {
 			teamsRoute.Delete("/:teamId/members/:userId", wrap(RemoveTeamMember))
 		}, reqOrgAdmin)
 
+		// team without requirement of user to be org admin
+		apiRoute.Group("/teams", func(teamsRoute RouteRegister) {
+			teamsRoute.Get("/:teamId", wrap(GetTeamByID))
+			teamsRoute.Get("/search", wrap(SearchTeams))
+		})
+
 		// org information available to all users.
 		apiRoute.Group("/org", func(orgRoute RouteRegister) {
 			orgRoute.Get("/", wrap(GetOrgCurrent))
@@ -170,7 +174,6 @@ func (hs *HTTPServer) registerRoutes() {
 			orgRoute.Put("/", bind(dtos.UpdateOrgForm{}), wrap(UpdateOrgCurrent))
 			orgRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), wrap(UpdateOrgAddressCurrent))
 			orgRoute.Post("/users", quota("user"), bind(m.AddOrgUserCommand{}), wrap(AddOrgUserToCurrentOrg))
-			orgRoute.Get("/users", wrap(GetOrgUsersForCurrentOrg))
 			orgRoute.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUserForCurrentOrg))
 			orgRoute.Delete("/users/:userId", wrap(RemoveOrgUserForCurrentOrg))
 
@@ -184,6 +187,11 @@ func (hs *HTTPServer) registerRoutes() {
 			orgRoute.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), wrap(UpdateOrgPreferences))
 		}, reqOrgAdmin)
 
+		// current org without requirement of user to be org admin
+		apiRoute.Group("/org", func(orgRoute RouteRegister) {
+			orgRoute.Get("/users", wrap(GetOrgUsersForCurrentOrg))
+		})
+
 		// create new org
 		apiRoute.Post("/orgs", quota("org"), bind(m.CreateOrgCommand{}), wrap(CreateOrg))
 

+ 2 - 5
pkg/api/avatar/avatar.go

@@ -258,9 +258,6 @@ func (this *thunderTask) fetch() error {
 	this.Avatar.data = &bytes.Buffer{}
 	writer := bufio.NewWriter(this.Avatar.data)
 
-	if _, err = io.Copy(writer, resp.Body); err != nil {
-		return err
-	}
-
-	return nil
+	_, err = io.Copy(writer, resp.Body)
+	return err
 }

+ 12 - 1
pkg/api/dashboard.go

@@ -102,6 +102,16 @@ func GetDashboard(c *m.ReqContext) Response {
 		meta.FolderUrl = query.Result.GetUrl()
 	}
 
+	isDashboardProvisioned := &m.IsDashboardProvisionedQuery{DashboardId: dash.Id}
+	err = bus.Dispatch(isDashboardProvisioned)
+	if err != nil {
+		return Error(500, "Error while checking if dashboard is provisioned", err)
+	}
+
+	if isDashboardProvisioned.Result {
+		meta.Provisioned = true
+	}
+
 	// make sure db version is in sync with json model version
 	dash.Data.Set("version", dash.Version)
 
@@ -228,7 +238,8 @@ func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
 		err == m.ErrDashboardWithSameUIDExists ||
 		err == m.ErrFolderNotFound ||
 		err == m.ErrDashboardFolderCannotHaveParent ||
-		err == m.ErrDashboardFolderNameExists {
+		err == m.ErrDashboardFolderNameExists ||
+		err == m.ErrDashboardCannotSaveProvisionedDashboard {
 		return Error(400, err.Error(), nil)
 	}
 

+ 5 - 0
pkg/api/dashboard_permission.go

@@ -29,6 +29,11 @@ func GetDashboardPermissionList(c *m.ReqContext) Response {
 	}
 
 	for _, perm := range acl {
+		perm.UserAvatarUrl = dtos.GetGravatarUrl(perm.UserEmail)
+
+		if perm.TeamId > 0 {
+			perm.TeamAvatarUrl = dtos.GetGravatarUrlWithDefault(perm.TeamEmail, perm.Team)
+		}
 		if perm.Slug != "" {
 			perm.Url = m.GetDashboardFolderUrl(perm.IsFolder, perm.Uid, perm.Slug)
 		}

+ 1 - 1
pkg/api/dashboard_permission_test.go

@@ -143,7 +143,7 @@ func TestDashboardPermissionApiEndpoint(t *testing.T) {
 			})
 		})
 
-		Convey("When trying to override inherited permissions with lower presedence", func() {
+		Convey("When trying to override inherited permissions with lower precedence", func() {
 			origNewGuardian := guardian.New
 			guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
 				CanAdminValue:                    true,

+ 21 - 0
pkg/api/dashboard_test.go

@@ -42,6 +42,11 @@ func TestDashboardApiEndpoint(t *testing.T) {
 			return nil
 		})
 
+		bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error {
+			query.Result = false
+			return nil
+		})
+
 		viewerRole := m.ROLE_VIEWER
 		editorRole := m.ROLE_EDITOR
 
@@ -192,6 +197,11 @@ func TestDashboardApiEndpoint(t *testing.T) {
 		fakeDash.HasAcl = true
 		setting.ViewersCanEdit = false
 
+		bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error {
+			query.Result = false
+			return nil
+		})
+
 		bus.AddHandler("test", func(query *m.GetDashboardsBySlugQuery) error {
 			dashboards := []*m.Dashboard{fakeDash}
 			query.Result = dashboards
@@ -625,6 +635,11 @@ func TestDashboardApiEndpoint(t *testing.T) {
 		dashTwo.FolderId = 3
 		dashTwo.HasAcl = false
 
+		bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error {
+			query.Result = false
+			return nil
+		})
+
 		bus.AddHandler("test", func(query *m.GetDashboardsBySlugQuery) error {
 			dashboards := []*m.Dashboard{dashOne, dashTwo}
 			query.Result = dashboards
@@ -720,6 +735,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
 				{SaveError: m.ErrDashboardUpdateAccessDenied, ExpectedStatusCode: 403},
 				{SaveError: m.ErrDashboardInvalidUid, ExpectedStatusCode: 400},
 				{SaveError: m.ErrDashboardUidToLong, ExpectedStatusCode: 400},
+				{SaveError: m.ErrDashboardCannotSaveProvisionedDashboard, ExpectedStatusCode: 400},
 				{SaveError: m.UpdatePluginDashboardError{PluginId: "plug"}, ExpectedStatusCode: 412},
 			}
 
@@ -750,6 +766,11 @@ func TestDashboardApiEndpoint(t *testing.T) {
 			return nil
 		})
 
+		bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error {
+			query.Result = false
+			return nil
+		})
+
 		bus.AddHandler("test", func(query *m.GetDashboardVersionQuery) error {
 			query.Result = &m.DashboardVersion{
 				Data: simplejson.NewFromAny(map[string]interface{}{

+ 1 - 0
pkg/api/dtos/dashboard.go

@@ -28,6 +28,7 @@ type DashboardMeta struct {
 	FolderId    int64     `json:"folderId"`
 	FolderTitle string    `json:"folderTitle"`
 	FolderUrl   string    `json:"folderUrl"`
+	Provisioned bool      `json:"provisioned"`
 }
 
 type DashboardFullWithMeta struct {

+ 6 - 0
pkg/api/folder_permission.go

@@ -33,6 +33,12 @@ func GetFolderPermissionList(c *m.ReqContext) Response {
 		perm.FolderId = folder.Id
 		perm.DashboardId = 0
 
+		perm.UserAvatarUrl = dtos.GetGravatarUrl(perm.UserEmail)
+
+		if perm.TeamId > 0 {
+			perm.TeamAvatarUrl = dtos.GetGravatarUrlWithDefault(perm.TeamEmail, perm.Team)
+		}
+
 		if perm.Slug != "" {
 			perm.Url = m.GetDashboardFolderUrl(perm.IsFolder, perm.Uid, perm.Slug)
 		}

+ 1 - 1
pkg/api/http_server.go

@@ -139,7 +139,7 @@ func (hs *HTTPServer) listenAndServeTLS(certfile, keyfile string) error {
 	}
 
 	hs.httpSrv.TLSConfig = tlsCfg
-	hs.httpSrv.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0)
+	hs.httpSrv.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
 
 	return hs.httpSrv.ListenAndServeTLS(setting.CertFile, setting.KeyFile)
 }

+ 1 - 1
pkg/cmd/grafana-cli/commands/install_command.go

@@ -33,7 +33,7 @@ func validateInput(c CommandLine, pluginFolder string) error {
 	fileInfo, err := os.Stat(pluginsDir)
 	if err != nil {
 		if err = os.MkdirAll(pluginsDir, os.ModePerm); err != nil {
-			return errors.New(fmt.Sprintf("pluginsDir (%s) is not a writable directory", pluginsDir))
+			return fmt.Errorf("pluginsDir (%s) is not a writable directory", pluginsDir)
 		}
 		return nil
 	}

+ 1 - 1
pkg/cmd/grafana-cli/commands/ls_command.go

@@ -24,7 +24,7 @@ var validateLsCommand = func(pluginDir string) error {
 		return fmt.Errorf("error: %s", err)
 	}
 
-	if pluginDirInfo.IsDir() == false {
+	if !pluginDirInfo.IsDir() {
 		return errors.New("plugin path is not a directory")
 	}
 

+ 1 - 2
pkg/cmd/grafana-cli/commands/upgrade_all_command.go

@@ -53,8 +53,7 @@ func upgradeAllCommand(c CommandLine) error {
 	for _, p := range pluginsToUpgrade {
 		logger.Infof("Updating %v \n", p.Id)
 
-		var err error
-		err = s.RemoveInstalledPlugin(pluginsDir, p.Id)
+		err := s.RemoveInstalledPlugin(pluginsDir, p.Id)
 		if err != nil {
 			return err
 		}

+ 1 - 1
pkg/cmd/grafana-cli/services/services.go

@@ -42,7 +42,7 @@ func Init(version string, skipTLSVerify bool) {
 	}
 
 	HttpClient = http.Client{
-		Timeout:   time.Duration(10 * time.Second),
+		Timeout:   10 * time.Second,
 		Transport: tr,
 	}
 }

+ 2 - 2
pkg/components/apikeygen/apikeygen.go

@@ -33,7 +33,7 @@ func New(orgId int64, name string) KeyGenResult {
 
 	jsonString, _ := json.Marshal(jsonKey)
 
-	result.ClientSecret = base64.StdEncoding.EncodeToString([]byte(jsonString))
+	result.ClientSecret = base64.StdEncoding.EncodeToString(jsonString)
 	return result
 }
 
@@ -44,7 +44,7 @@ func Decode(keyString string) (*ApiKeyJson, error) {
 	}
 
 	var keyObj ApiKeyJson
-	err = json.Unmarshal([]byte(jsonString), &keyObj)
+	err = json.Unmarshal(jsonString, &keyObj)
 	if err != nil {
 		return nil, ErrInvalidApiKey
 	}

+ 0 - 7
pkg/components/dynmap/dynmap.go

@@ -585,7 +585,6 @@ func (v *Value) Null() error {
 	switch v.data.(type) {
 	case nil:
 		valid = v.exists // Valid only if j also exists, since other values could possibly also be nil
-		break
 	}
 
 	if valid {
@@ -607,7 +606,6 @@ func (v *Value) Array() ([]*Value, error) {
 	switch v.data.(type) {
 	case []interface{}:
 		valid = true
-		break
 	}
 
 	// Unsure if this is a good way to use slices, it's probably not
@@ -638,7 +636,6 @@ func (v *Value) Number() (json.Number, error) {
 	switch v.data.(type) {
 	case json.Number:
 		valid = true
-		break
 	}
 
 	if valid {
@@ -687,7 +684,6 @@ func (v *Value) Boolean() (bool, error) {
 	switch v.data.(type) {
 	case bool:
 		valid = true
-		break
 	}
 
 	if valid {
@@ -709,7 +705,6 @@ func (v *Value) Object() (*Object, error) {
 	switch v.data.(type) {
 	case map[string]interface{}:
 		valid = true
-		break
 	}
 
 	if valid {
@@ -746,7 +741,6 @@ func (v *Value) ObjectArray() ([]*Object, error) {
 	switch v.data.(type) {
 	case []interface{}:
 		valid = true
-		break
 	}
 
 	// Unsure if this is a good way to use slices, it's probably not
@@ -782,7 +776,6 @@ func (v *Value) String() (string, error) {
 	switch v.data.(type) {
 	case string:
 		valid = true
-		break
 	}
 
 	if valid {

+ 4 - 4
pkg/components/dynmap/dynmap_test.go

@@ -21,7 +21,7 @@ func NewAssert(t *testing.T) *Assert {
 }
 
 func (assert *Assert) True(value bool, message string) {
-	if value == false {
+	if !value {
 		log.Panicln("Assert: ", message)
 	}
 }
@@ -119,13 +119,13 @@ func TestFirst(t *testing.T) {
 	assert.True(s == "" && err != nil, "nonexistent string fail")
 
 	b, err := j.GetBoolean("true")
-	assert.True(b == true && err == nil, "bool true test")
+	assert.True(b && err == nil, "bool true test")
 
 	b, err = j.GetBoolean("false")
-	assert.True(b == false && err == nil, "bool false test")
+	assert.True(!b && err == nil, "bool false test")
 
 	b, err = j.GetBoolean("invalid_field")
-	assert.True(b == false && err != nil, "bool invalid test")
+	assert.True(!b && err != nil, "bool invalid test")
 
 	list, err := j.GetValueArray("list")
 	assert.True(list != nil && err == nil, "list should be an array")

+ 1 - 1
pkg/components/imguploader/azureblobuploader.go

@@ -225,7 +225,7 @@ func (a *Auth) SignRequest(req *http.Request) {
 	)
 	decodedKey, _ := base64.StdEncoding.DecodeString(a.Key)
 
-	sha256 := hmac.New(sha256.New, []byte(decodedKey))
+	sha256 := hmac.New(sha256.New, decodedKey)
 	sha256.Write([]byte(strToSign))
 
 	signature := base64.StdEncoding.EncodeToString(sha256.Sum(nil))

+ 1 - 1
pkg/components/null/float.go

@@ -50,7 +50,7 @@ func (f *Float) UnmarshalJSON(data []byte) error {
 	}
 	switch x := v.(type) {
 	case float64:
-		f.Float64 = float64(x)
+		f.Float64 = x
 	case map[string]interface{}:
 		err = json.Unmarshal(data, &f.NullFloat64)
 	case nil:

+ 1 - 4
pkg/log/file.go

@@ -99,10 +99,7 @@ func (w *FileLogWriter) StartLogger() error {
 		return err
 	}
 	w.mw.SetFd(fd)
-	if err = w.initFd(); err != nil {
-		return err
-	}
-	return nil
+	return w.initFd()
 }
 
 func (w *FileLogWriter) docheck(size int) {

+ 1 - 2
pkg/login/ldap.go

@@ -403,8 +403,7 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
 		// If we are using a POSIX LDAP schema it won't support memberOf, so we manually search the groups
 		var groupSearchResult *ldap.SearchResult
 		for _, groupSearchBase := range a.server.GroupSearchBaseDNs {
-			var filter_replace string
-			filter_replace = getLdapAttr(a.server.GroupSearchFilterUserAttribute, searchResult)
+			filter_replace := getLdapAttr(a.server.GroupSearchFilterUserAttribute, searchResult)
 			if a.server.GroupSearchFilterUserAttribute == "" {
 				filter_replace = getLdapAttr(a.server.Attr.Username, searchResult)
 			}

+ 1 - 5
pkg/metrics/graphitebridge/graphite.go

@@ -295,11 +295,7 @@ func writeMetric(buf *bufio.Writer, m model.Metric, mf *dto.MetricFamily) error
 		}
 	}
 
-	if err = addExtentionConventionForRollups(buf, mf, m); err != nil {
-		return err
-	}
-
-	return nil
+	return addExtentionConventionForRollups(buf, mf, m)
 }
 
 func addExtentionConventionForRollups(buf *bufio.Writer, mf *dto.MetricFamily, m model.Metric) error {

+ 26 - 18
pkg/metrics/metrics.go

@@ -54,6 +54,7 @@ var (
 	M_Alerting_Active_Alerts prometheus.Gauge
 	M_StatTotal_Dashboards   prometheus.Gauge
 	M_StatTotal_Users        prometheus.Gauge
+	M_StatActive_Users       prometheus.Gauge
 	M_StatTotal_Orgs         prometheus.Gauge
 	M_StatTotal_Playlists    prometheus.Gauge
 	M_Grafana_Version        *prometheus.GaugeVec
@@ -253,6 +254,12 @@ func init() {
 		Namespace: exporterName,
 	})
 
+	M_StatActive_Users = prometheus.NewGauge(prometheus.GaugeOpts{
+		Name:      "stat_active_users",
+		Help:      "number of active users",
+		Namespace: exporterName,
+	})
+
 	M_StatTotal_Orgs = prometheus.NewGauge(prometheus.GaugeOpts{
 		Name:      "stat_total_orgs",
 		Help:      "total amount of orgs",
@@ -270,7 +277,6 @@ func init() {
 		Help:      "Information about the Grafana",
 		Namespace: exporterName,
 	}, []string{"version"})
-
 }
 
 func initMetricVars(settings *MetricSettings) {
@@ -305,6 +311,7 @@ func initMetricVars(settings *MetricSettings) {
 		M_Alerting_Active_Alerts,
 		M_StatTotal_Dashboards,
 		M_StatTotal_Users,
+		M_StatActive_Users,
 		M_StatTotal_Orgs,
 		M_StatTotal_Playlists,
 		M_Grafana_Version)
@@ -315,35 +322,36 @@ func initMetricVars(settings *MetricSettings) {
 func instrumentationLoop(settings *MetricSettings) chan struct{} {
 	M_Instance_Start.Inc()
 
+	// set the total stats gauges before we publishing metrics
+	updateTotalStats()
+
 	onceEveryDayTick := time.NewTicker(time.Hour * 24)
-	secondTicker := time.NewTicker(time.Second * time.Duration(settings.IntervalSeconds))
+	everyMinuteTicker := time.NewTicker(time.Minute)
+	defer onceEveryDayTick.Stop()
+	defer everyMinuteTicker.Stop()
 
 	for {
 		select {
 		case <-onceEveryDayTick.C:
 			sendUsageStats()
-		case <-secondTicker.C:
+		case <-everyMinuteTicker.C:
 			updateTotalStats()
 		}
 	}
 }
 
-var metricPublishCounter int64 = 0
-
 func updateTotalStats() {
-	metricPublishCounter++
-	if metricPublishCounter == 1 || metricPublishCounter%10 == 0 {
-		statsQuery := models.GetSystemStatsQuery{}
-		if err := bus.Dispatch(&statsQuery); err != nil {
-			metricsLogger.Error("Failed to get system stats", "error", err)
-			return
-		}
-
-		M_StatTotal_Dashboards.Set(float64(statsQuery.Result.Dashboards))
-		M_StatTotal_Users.Set(float64(statsQuery.Result.Users))
-		M_StatTotal_Playlists.Set(float64(statsQuery.Result.Playlists))
-		M_StatTotal_Orgs.Set(float64(statsQuery.Result.Orgs))
+	statsQuery := models.GetSystemStatsQuery{}
+	if err := bus.Dispatch(&statsQuery); err != nil {
+		metricsLogger.Error("Failed to get system stats", "error", err)
+		return
 	}
+
+	M_StatTotal_Dashboards.Set(float64(statsQuery.Result.Dashboards))
+	M_StatTotal_Users.Set(float64(statsQuery.Result.Users))
+	M_StatActive_Users.Set(float64(statsQuery.Result.ActiveUsers))
+	M_StatTotal_Playlists.Set(float64(statsQuery.Result.Playlists))
+	M_StatTotal_Orgs.Set(float64(statsQuery.Result.Orgs))
 }
 
 func sendUsageStats() {
@@ -403,6 +411,6 @@ func sendUsageStats() {
 	out, _ := json.MarshalIndent(report, "", " ")
 	data := bytes.NewBuffer(out)
 
-	client := http.Client{Timeout: time.Duration(5 * time.Second)}
+	client := http.Client{Timeout: 5 * time.Second}
 	go client.Post("https://stats.grafana.org/grafana-usage-report", "application/json", data)
 }

+ 3 - 0
pkg/models/dashboard_acl.go

@@ -56,7 +56,10 @@ type DashboardAclInfoDTO struct {
 	UserId         int64          `json:"userId"`
 	UserLogin      string         `json:"userLogin"`
 	UserEmail      string         `json:"userEmail"`
+	UserAvatarUrl  string         `json:"userAvatarUrl"`
 	TeamId         int64          `json:"teamId"`
+	TeamEmail      string         `json:"teamEmail"`
+	TeamAvatarUrl  string         `json:"teamAvatarUrl"`
 	Team           string         `json:"team"`
 	Role           *RoleType      `json:"role,omitempty"`
 	Permission     PermissionType `json:"permission"`

+ 32 - 20
pkg/models/dashboards.go

@@ -13,26 +13,27 @@ import (
 
 // Typed errors
 var (
-	ErrDashboardNotFound                      = errors.New("Dashboard not found")
-	ErrDashboardFolderNotFound                = errors.New("Folder not found")
-	ErrDashboardSnapshotNotFound              = errors.New("Dashboard snapshot not found")
-	ErrDashboardWithSameUIDExists             = errors.New("A dashboard with the same uid already exists")
-	ErrDashboardWithSameNameInFolderExists    = errors.New("A dashboard with the same name in the folder already exists")
-	ErrDashboardVersionMismatch               = errors.New("The dashboard has been changed by someone else")
-	ErrDashboardTitleEmpty                    = errors.New("Dashboard title cannot be empty")
-	ErrDashboardFolderCannotHaveParent        = errors.New("A Dashboard Folder cannot be added to another folder")
-	ErrDashboardContainsInvalidAlertData      = errors.New("Invalid alert data. Cannot save dashboard")
-	ErrDashboardFailedToUpdateAlertData       = errors.New("Failed to save alert data")
-	ErrDashboardsWithSameSlugExists           = errors.New("Multiple dashboards with the same slug exists")
-	ErrDashboardFailedGenerateUniqueUid       = errors.New("Failed to generate unique dashboard id")
-	ErrDashboardTypeMismatch                  = errors.New("Dashboard cannot be changed to a folder")
-	ErrDashboardFolderWithSameNameAsDashboard = errors.New("Folder name cannot be the same as one of its dashboards")
-	ErrDashboardWithSameNameAsFolder          = errors.New("Dashboard name cannot be the same as folder")
-	ErrDashboardFolderNameExists              = errors.New("A folder with that name already exists")
-	ErrDashboardUpdateAccessDenied            = errors.New("Access denied to save dashboard")
-	ErrDashboardInvalidUid                    = errors.New("uid contains illegal characters")
-	ErrDashboardUidToLong                     = errors.New("uid to long. max 40 characters")
-	RootFolderName                            = "General"
+	ErrDashboardNotFound                       = errors.New("Dashboard not found")
+	ErrDashboardFolderNotFound                 = errors.New("Folder not found")
+	ErrDashboardSnapshotNotFound               = errors.New("Dashboard snapshot not found")
+	ErrDashboardWithSameUIDExists              = errors.New("A dashboard with the same uid already exists")
+	ErrDashboardWithSameNameInFolderExists     = errors.New("A dashboard with the same name in the folder already exists")
+	ErrDashboardVersionMismatch                = errors.New("The dashboard has been changed by someone else")
+	ErrDashboardTitleEmpty                     = errors.New("Dashboard title cannot be empty")
+	ErrDashboardFolderCannotHaveParent         = errors.New("A Dashboard Folder cannot be added to another folder")
+	ErrDashboardContainsInvalidAlertData       = errors.New("Invalid alert data. Cannot save dashboard")
+	ErrDashboardFailedToUpdateAlertData        = errors.New("Failed to save alert data")
+	ErrDashboardsWithSameSlugExists            = errors.New("Multiple dashboards with the same slug exists")
+	ErrDashboardFailedGenerateUniqueUid        = errors.New("Failed to generate unique dashboard id")
+	ErrDashboardTypeMismatch                   = errors.New("Dashboard cannot be changed to a folder")
+	ErrDashboardFolderWithSameNameAsDashboard  = errors.New("Folder name cannot be the same as one of its dashboards")
+	ErrDashboardWithSameNameAsFolder           = errors.New("Dashboard name cannot be the same as folder")
+	ErrDashboardFolderNameExists               = errors.New("A folder with that name already exists")
+	ErrDashboardUpdateAccessDenied             = errors.New("Access denied to save dashboard")
+	ErrDashboardInvalidUid                     = errors.New("uid contains illegal characters")
+	ErrDashboardUidToLong                      = errors.New("uid to long. max 40 characters")
+	ErrDashboardCannotSaveProvisionedDashboard = errors.New("Cannot save provisioned dashboard")
+	RootFolderName                             = "General"
 )
 
 type UpdatePluginDashboardError struct {
@@ -224,6 +225,10 @@ func GetFolderUrl(folderUid string, slug string) string {
 	return fmt.Sprintf("%s/dashboards/f/%s/%s", setting.AppSubUrl, folderUid, slug)
 }
 
+type ValidateDashboardBeforeSaveResult struct {
+	IsParentFolderChanged bool
+}
+
 //
 // COMMANDS
 //
@@ -268,6 +273,7 @@ type ValidateDashboardBeforeSaveCommand struct {
 	OrgId     int64
 	Dashboard *Dashboard
 	Overwrite bool
+	Result    *ValidateDashboardBeforeSaveResult
 }
 
 //
@@ -317,6 +323,12 @@ type GetDashboardSlugByIdQuery struct {
 	Result string
 }
 
+type IsDashboardProvisionedQuery struct {
+	DashboardId int64
+
+	Result bool
+}
+
 type GetProvisionedDashboardDataQuery struct {
 	Name string
 

+ 1 - 1
pkg/models/datasource_cache.go

@@ -33,7 +33,7 @@ func (ds *DataSource) GetHttpClient() (*http.Client, error) {
 	}
 
 	return &http.Client{
-		Timeout:   time.Duration(30 * time.Second),
+		Timeout:   30 * time.Second,
 		Transport: transport,
 	}, nil
 }

+ 2 - 2
pkg/models/org_user.go

@@ -48,9 +48,9 @@ func (r *RoleType) UnmarshalJSON(data []byte) error {
 
 	*r = RoleType(str)
 
-	if (*r).IsValid() == false {
+	if !(*r).IsValid() {
 		if (*r) != "" {
-			return errors.New(fmt.Sprintf("JSON validation error: invalid role value: %s", *r))
+			return fmt.Errorf("JSON validation error: invalid role value: %s", *r)
 		}
 
 		*r = ROLE_VIEWER

+ 1 - 1
pkg/plugins/datasource/wrapper/datasource_plugin_wrapper_test.go

@@ -74,7 +74,7 @@ func TestMappingRowValue(t *testing.T) {
 
 	boolRowValue, _ := dpw.mapRowValue(&datasource.RowValue{Kind: datasource.RowValue_TYPE_BOOL, BoolValue: true})
 	haveBool, ok := boolRowValue.(bool)
-	if !ok || haveBool != true {
+	if !ok || !haveBool {
 		t.Fatalf("Expected true, was %v", haveBool)
 	}
 

+ 1 - 1
pkg/plugins/models.go

@@ -69,7 +69,7 @@ func (pb *PluginBase) registerPlugin(pluginDir string) error {
 
 	for _, include := range pb.Includes {
 		if include.Role == "" {
-			include.Role = m.RoleType(m.ROLE_VIEWER)
+			include.Role = m.ROLE_VIEWER
 		}
 	}
 

+ 1 - 1
pkg/plugins/update_checker.go

@@ -13,7 +13,7 @@ import (
 )
 
 var (
-	httpClient http.Client = http.Client{Timeout: time.Duration(10 * time.Second)}
+	httpClient http.Client = http.Client{Timeout: 10 * time.Second}
 )
 
 type GrafanaNetPlugin struct {

+ 3 - 3
pkg/services/alerting/conditions/evaluator.go

@@ -20,7 +20,7 @@ type AlertEvaluator interface {
 type NoValueEvaluator struct{}
 
 func (e *NoValueEvaluator) Eval(reducedValue null.Float) bool {
-	return reducedValue.Valid == false
+	return !reducedValue.Valid
 }
 
 type ThresholdEvaluator struct {
@@ -45,7 +45,7 @@ func newThresholdEvaluator(typ string, model *simplejson.Json) (*ThresholdEvalua
 }
 
 func (e *ThresholdEvaluator) Eval(reducedValue null.Float) bool {
-	if reducedValue.Valid == false {
+	if !reducedValue.Valid {
 		return false
 	}
 
@@ -88,7 +88,7 @@ func newRangedEvaluator(typ string, model *simplejson.Json) (*RangedEvaluator, e
 }
 
 func (e *RangedEvaluator) Eval(reducedValue null.Float) bool {
-	if reducedValue.Valid == false {
+	if !reducedValue.Valid {
 		return false
 	}
 

+ 1 - 1
pkg/services/alerting/conditions/query.go

@@ -53,7 +53,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio
 		reducedValue := c.Reducer.Reduce(series)
 		evalMatch := c.Evaluator.Eval(reducedValue)
 
-		if reducedValue.Valid == false {
+		if !reducedValue.Valid {
 			emptySerieCount++
 		}
 

+ 1 - 1
pkg/services/alerting/extractor.go

@@ -104,7 +104,7 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
 
 		// backward compatibility check, can be removed later
 		enabled, hasEnabled := jsonAlert.CheckGet("enabled")
-		if hasEnabled && enabled.MustBool() == false {
+		if hasEnabled && !enabled.MustBool() {
 			continue
 		}
 

+ 1 - 1
pkg/services/alerting/notifiers/line.go

@@ -90,7 +90,7 @@ func (this *LineNotifier) createAlert(evalContext *alerting.EvalContext) error {
 	}
 
 	if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
-		this.log.Error("Failed to send notification to LINE", "error", err, "body", string(body))
+		this.log.Error("Failed to send notification to LINE", "error", err, "body", body)
 		return err
 	}
 

+ 1 - 1
pkg/services/alerting/notifiers/telegram.go

@@ -219,7 +219,7 @@ func appendIfPossible(message string, extra string, sizeLimit int) string {
 
 func (this *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error {
 	var cmd *m.SendWebhookSync
-	if evalContext.ImagePublicUrl == "" && this.UploadImage == true {
+	if evalContext.ImagePublicUrl == "" && this.UploadImage {
 		cmd = this.buildMessage(evalContext, true)
 	} else {
 		cmd = this.buildMessage(evalContext, false)

+ 2 - 2
pkg/services/alerting/rule.go

@@ -55,8 +55,8 @@ func (e ValidationError) Error() string {
 }
 
 var (
-	ValueFormatRegex = regexp.MustCompile("^\\d+")
-	UnitFormatRegex  = regexp.MustCompile("\\w{1}$")
+	ValueFormatRegex = regexp.MustCompile(`^\d+`)
+	UnitFormatRegex  = regexp.MustCompile(`\w{1}$`)
 )
 
 var unitMultiplier = map[string]int{

+ 2 - 2
pkg/services/alerting/scheduler.go

@@ -15,7 +15,7 @@ type SchedulerImpl struct {
 
 func NewScheduler() Scheduler {
 	return &SchedulerImpl{
-		jobs: make(map[int64]*Job, 0),
+		jobs: make(map[int64]*Job),
 		log:  log.New("alerting.scheduler"),
 	}
 }
@@ -23,7 +23,7 @@ func NewScheduler() Scheduler {
 func (s *SchedulerImpl) Update(rules []*Rule) {
 	s.log.Debug("Scheduling update", "ruleCount", len(rules))
 
-	jobs := make(map[int64]*Job, 0)
+	jobs := make(map[int64]*Job)
 
 	for i, rule := range rules {
 		var job *Job

+ 28 - 5
pkg/services/dashboards/dashboard_service.go

@@ -57,7 +57,7 @@ func (dr *dashboardServiceImpl) GetProvisionedDashboardData(name string) ([]*mod
 	return cmd.Result, nil
 }
 
-func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, validateAlerts bool) (*models.SaveDashboardCommand, error) {
+func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, validateAlerts bool, validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error) {
 	dash := dto.Dashboard
 
 	dash.Title = strings.TrimSpace(dash.Title)
@@ -103,6 +103,29 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO,
 		return nil, err
 	}
 
+	if validateBeforeSaveCmd.Result.IsParentFolderChanged {
+		folderGuardian := guardian.New(dash.FolderId, dto.OrgId, dto.User)
+		if canSave, err := folderGuardian.CanSave(); err != nil || !canSave {
+			if err != nil {
+				return nil, err
+			}
+			return nil, models.ErrDashboardUpdateAccessDenied
+		}
+	}
+
+	if validateProvisionedDashboard {
+		isDashboardProvisioned := &models.IsDashboardProvisionedQuery{DashboardId: dash.Id}
+		err := bus.Dispatch(isDashboardProvisioned)
+
+		if err != nil {
+			return nil, err
+		}
+
+		if isDashboardProvisioned.Result {
+			return nil, models.ErrDashboardCannotSaveProvisionedDashboard
+		}
+	}
+
 	guard := guardian.New(dash.GetDashboardIdForSavePermissionCheck(), dto.OrgId, dto.User)
 	if canSave, err := guard.CanSave(); err != nil || !canSave {
 		if err != nil {
@@ -148,7 +171,7 @@ func (dr *dashboardServiceImpl) SaveProvisionedDashboard(dto *SaveDashboardDTO,
 		UserId:  0,
 		OrgRole: models.ROLE_ADMIN,
 	}
-	cmd, err := dr.buildSaveDashboardCommand(dto, true)
+	cmd, err := dr.buildSaveDashboardCommand(dto, true, false)
 	if err != nil {
 		return nil, err
 	}
@@ -178,7 +201,7 @@ func (dr *dashboardServiceImpl) SaveFolderForProvisionedDashboards(dto *SaveDash
 		UserId:  0,
 		OrgRole: models.ROLE_ADMIN,
 	}
-	cmd, err := dr.buildSaveDashboardCommand(dto, false)
+	cmd, err := dr.buildSaveDashboardCommand(dto, false, false)
 	if err != nil {
 		return nil, err
 	}
@@ -197,7 +220,7 @@ func (dr *dashboardServiceImpl) SaveFolderForProvisionedDashboards(dto *SaveDash
 }
 
 func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
-	cmd, err := dr.buildSaveDashboardCommand(dto, true)
+	cmd, err := dr.buildSaveDashboardCommand(dto, true, true)
 	if err != nil {
 		return nil, err
 	}
@@ -216,7 +239,7 @@ func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO) (*models.Da
 }
 
 func (dr *dashboardServiceImpl) ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
-	cmd, err := dr.buildSaveDashboardCommand(dto, false)
+	cmd, err := dr.buildSaveDashboardCommand(dto, false, true)
 	if err != nil {
 		return nil, err
 	}

+ 114 - 2
pkg/services/dashboards/dashboard_service_test.go

@@ -14,7 +14,9 @@ import (
 
 func TestDashboardService(t *testing.T) {
 	Convey("Dashboard service tests", t, func() {
-		service := dashboardServiceImpl{}
+		bus.ClearBusHandlers()
+
+		service := &dashboardServiceImpl{}
 
 		origNewDashboardGuardian := guardian.New
 		guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true})
@@ -51,6 +53,12 @@ func TestDashboardService(t *testing.T) {
 				})
 
 				bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
+					cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
+					return nil
+				})
+
+				bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
+					cmd.Result = false
 					return nil
 				})
 
@@ -72,12 +80,42 @@ func TestDashboardService(t *testing.T) {
 					dto.Dashboard.SetUid(tc.Uid)
 					dto.User = &models.SignedInUser{}
 
-					_, err := service.buildSaveDashboardCommand(dto, true)
+					_, err := service.buildSaveDashboardCommand(dto, true, false)
 					So(err, ShouldEqual, tc.Error)
 				}
 			})
 
+			Convey("Should return validation error if dashboard is provisioned", func() {
+				provisioningValidated := false
+				bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
+					provisioningValidated = true
+					cmd.Result = true
+					return nil
+				})
+
+				bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
+					return nil
+				})
+
+				bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
+					cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
+					return nil
+				})
+
+				dto.Dashboard = models.NewDashboard("Dash")
+				dto.Dashboard.SetId(3)
+				dto.User = &models.SignedInUser{UserId: 1}
+				_, err := service.SaveDashboard(dto)
+				So(provisioningValidated, ShouldBeTrue)
+				So(err, ShouldEqual, models.ErrDashboardCannotSaveProvisionedDashboard)
+			})
+
 			Convey("Should return validation error if alert data is invalid", func() {
+				bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
+					cmd.Result = false
+					return nil
+				})
+
 				bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
 					return errors.New("error")
 				})
@@ -88,6 +126,80 @@ func TestDashboardService(t *testing.T) {
 			})
 		})
 
+		Convey("Save provisioned dashboard validation", func() {
+			dto := &SaveDashboardDTO{}
+
+			Convey("Should not return validation error if dashboard is provisioned", func() {
+				provisioningValidated := false
+				bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
+					provisioningValidated = true
+					cmd.Result = true
+					return nil
+				})
+
+				bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
+					return nil
+				})
+
+				bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
+					cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
+					return nil
+				})
+
+				bus.AddHandler("test", func(cmd *models.SaveProvisionedDashboardCommand) error {
+					return nil
+				})
+
+				bus.AddHandler("test", func(cmd *models.UpdateDashboardAlertsCommand) error {
+					return nil
+				})
+
+				dto.Dashboard = models.NewDashboard("Dash")
+				dto.Dashboard.SetId(3)
+				dto.User = &models.SignedInUser{UserId: 1}
+				_, err := service.SaveProvisionedDashboard(dto, nil)
+				So(err, ShouldBeNil)
+				So(provisioningValidated, ShouldBeFalse)
+			})
+		})
+
+		Convey("Import dashboard validation", func() {
+			dto := &SaveDashboardDTO{}
+
+			Convey("Should return validation error if dashboard is provisioned", func() {
+				provisioningValidated := false
+				bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
+					provisioningValidated = true
+					cmd.Result = true
+					return nil
+				})
+
+				bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
+					return nil
+				})
+
+				bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
+					cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
+					return nil
+				})
+
+				bus.AddHandler("test", func(cmd *models.SaveProvisionedDashboardCommand) error {
+					return nil
+				})
+
+				bus.AddHandler("test", func(cmd *models.UpdateDashboardAlertsCommand) error {
+					return nil
+				})
+
+				dto.Dashboard = models.NewDashboard("Dash")
+				dto.Dashboard.SetId(3)
+				dto.User = &models.SignedInUser{UserId: 1}
+				_, err := service.ImportDashboard(dto)
+				So(provisioningValidated, ShouldBeTrue)
+				So(err, ShouldEqual, models.ErrDashboardCannotSaveProvisionedDashboard)
+			})
+		})
+
 		Reset(func() {
 			guardian.New = origNewDashboardGuardian
 		})

+ 2 - 2
pkg/services/dashboards/folder_service.go

@@ -104,7 +104,7 @@ func (dr *dashboardServiceImpl) CreateFolder(cmd *models.CreateFolderCommand) er
 		User:      dr.user,
 	}
 
-	saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false)
+	saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false, false)
 	if err != nil {
 		return toFolderError(err)
 	}
@@ -141,7 +141,7 @@ func (dr *dashboardServiceImpl) UpdateFolder(existingUid string, cmd *models.Upd
 		Overwrite: cmd.Overwrite,
 	}
 
-	saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false)
+	saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false, false)
 	if err != nil {
 		return toFolderError(err)
 	}

+ 11 - 0
pkg/services/dashboards/folder_service_test.go

@@ -32,6 +32,7 @@ func TestFolderService(t *testing.T) {
 			})
 
 			bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
+				cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
 				return models.ErrDashboardUpdateAccessDenied
 			})
 
@@ -92,6 +93,7 @@ func TestFolderService(t *testing.T) {
 			})
 
 			bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
+				cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
 				return nil
 			})
 
@@ -108,11 +110,19 @@ func TestFolderService(t *testing.T) {
 				return nil
 			})
 
+			provisioningValidated := false
+
+			bus.AddHandler("test", func(query *models.IsDashboardProvisionedQuery) error {
+				provisioningValidated = true
+				return nil
+			})
+
 			Convey("When creating folder should not return access denied error", func() {
 				err := service.CreateFolder(&models.CreateFolderCommand{
 					Title: "Folder",
 				})
 				So(err, ShouldBeNil)
+				So(provisioningValidated, ShouldBeFalse)
 			})
 
 			Convey("When updating folder should not return access denied error", func() {
@@ -121,6 +131,7 @@ func TestFolderService(t *testing.T) {
 					Title: "Folder",
 				})
 				So(err, ShouldBeNil)
+				So(provisioningValidated, ShouldBeFalse)
 			})
 
 			Convey("When deleting folder by uid should not return access denied error", func() {

+ 1 - 1
pkg/services/guardian/guardian.go

@@ -173,7 +173,7 @@ func (g *dashboardGuardianImpl) CheckPermissionBeforeUpdate(permission m.Permiss
 		return true, nil
 	}
 
-	return g.checkAcl(permission, acl)
+	return g.checkAcl(permission, existingPermissions)
 }
 
 // GetAcl returns dashboard acl

+ 617 - 664
pkg/services/guardian/guardian_test.go

@@ -2,710 +2,663 @@ package guardian
 
 import (
 	"fmt"
+	"runtime"
 	"testing"
 
-	"github.com/grafana/grafana/pkg/bus"
-
 	m "github.com/grafana/grafana/pkg/models"
 	. "github.com/smartystreets/goconvey/convey"
 )
 
-func TestGuardian(t *testing.T) {
-	Convey("Guardian permission tests", t, func() {
-		orgRoleScenario("Given user has admin org role", m.ROLE_ADMIN, func(sc *scenarioContext) {
-			canAdmin, _ := sc.g.CanAdmin()
-			canEdit, _ := sc.g.CanEdit()
-			canSave, _ := sc.g.CanSave()
-			canView, _ := sc.g.CanView()
-			So(canAdmin, ShouldBeTrue)
-			So(canEdit, ShouldBeTrue)
-			So(canSave, ShouldBeTrue)
-			So(canView, ShouldBeTrue)
-
-			Convey("When trying to update permissions", func() {
-				Convey("With duplicate user permissions should return error", func() {
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_VIEW},
-						{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN},
-					}
-					_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(err, ShouldEqual, ErrGuardianPermissionExists)
-				})
-
-				Convey("With duplicate team permissions should return error", func() {
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_VIEW},
-						{OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_ADMIN},
-					}
-					_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(err, ShouldEqual, ErrGuardianPermissionExists)
-				})
-
-				Convey("With duplicate everyone with editor role permission should return error", func() {
-					r := m.ROLE_EDITOR
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_VIEW},
-						{OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_ADMIN},
-					}
-					_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(err, ShouldEqual, ErrGuardianPermissionExists)
-				})
-
-				Convey("With duplicate everyone with viewer role permission should return error", func() {
-					r := m.ROLE_VIEWER
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_VIEW},
-						{OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_ADMIN},
-					}
-					_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(err, ShouldEqual, ErrGuardianPermissionExists)
-				})
-
-				Convey("With everyone with admin role permission should return error", func() {
-					r := m.ROLE_ADMIN
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_ADMIN},
-					}
-					_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(err, ShouldEqual, ErrGuardianPermissionExists)
-				})
-			})
-
-			Convey("Given default permissions", func() {
-				editor := m.ROLE_EDITOR
-				viewer := m.ROLE_VIEWER
-				existingPermissions := []*m.DashboardAclInfoDTO{
-					{OrgId: 1, DashboardId: -1, Role: &editor, Permission: m.PERMISSION_EDIT},
-					{OrgId: 1, DashboardId: -1, Role: &viewer, Permission: m.PERMISSION_VIEW},
-				}
-
-				bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
-					query.Result = existingPermissions
-					return nil
-				})
-
-				Convey("When trying to update dashboard permissions without everyone with role editor can edit should be allowed", func() {
-					r := m.ROLE_VIEWER
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_VIEW},
-					}
-					ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(ok, ShouldBeTrue)
-				})
-
-				Convey("When trying to update dashboard permissions without everyone with role viewer can view should be allowed", func() {
-					r := m.ROLE_EDITOR
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_EDIT},
-					}
-					ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(ok, ShouldBeTrue)
-				})
-			})
-
-			Convey("Given parent folder has user admin permission", func() {
-				existingPermissions := []*m.DashboardAclInfoDTO{
-					{OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_ADMIN},
-				}
-
-				bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
-					query.Result = existingPermissions
-					return nil
-				})
-
-				Convey("When trying to update dashboard permissions with admin user permission should return error", func() {
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_ADMIN},
-					}
-					_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(err, ShouldEqual, ErrGuardianOverride)
-				})
-
-				Convey("When trying to update dashboard permissions with edit user permission should return error", func() {
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_EDIT},
-					}
-					_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(err, ShouldEqual, ErrGuardianOverride)
-				})
-
-				Convey("When trying to update dashboard permissions with view user permission should return error", func() {
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_VIEW},
-					}
-					_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(err, ShouldEqual, ErrGuardianOverride)
-				})
-			})
-
-			Convey("Given parent folder has user edit permission", func() {
-				existingPermissions := []*m.DashboardAclInfoDTO{
-					{OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_EDIT},
-				}
+var (
+	orgID              = int64(1)
+	defaultDashboardID = int64(-1)
+	dashboardID        = int64(1)
+	parentFolderID     = int64(2)
+	childDashboardID   = int64(3)
+	userID             = int64(1)
+	otherUserID        = int64(2)
+	teamID             = int64(1)
+	otherTeamID        = int64(2)
+	adminRole          = m.ROLE_ADMIN
+	editorRole         = m.ROLE_EDITOR
+	viewerRole         = m.ROLE_VIEWER
+)
 
-				bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
-					query.Result = existingPermissions
-					return nil
-				})
-
-				Convey("When trying to update dashboard permissions with admin user permission should be allowed", func() {
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_ADMIN},
-					}
-					ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(ok, ShouldBeTrue)
-				})
-
-				Convey("When trying to update dashboard permissions with edit user permission should return error", func() {
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_EDIT},
-					}
-					_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(err, ShouldEqual, ErrGuardianOverride)
-				})
-
-				Convey("When trying to update dashboard permissions with view user permission should return error", func() {
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_VIEW},
-					}
-					_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(err, ShouldEqual, ErrGuardianOverride)
-				})
-			})
-
-			Convey("Given parent folder has user view permission", func() {
-				existingPermissions := []*m.DashboardAclInfoDTO{
-					{OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW},
-				}
+func TestGuardianAdmin(t *testing.T) {
+	Convey("Guardian admin org role tests", t, func() {
+		orgRoleScenario("Given user has admin org role", t, m.ROLE_ADMIN, func(sc *scenarioContext) {
+			// dashboard has default permissions
+			sc.defaultPermissionScenario(USER, m.PERMISSION_ADMIN, FULL_ACCESS)
+
+			// dashboard has user with permission
+			sc.dashboardPermissionScenario(USER, m.PERMISSION_ADMIN, FULL_ACCESS)
+			sc.dashboardPermissionScenario(USER, m.PERMISSION_EDIT, FULL_ACCESS)
+			sc.dashboardPermissionScenario(USER, m.PERMISSION_VIEW, FULL_ACCESS)
+
+			// dashboard has team with permission
+			sc.dashboardPermissionScenario(TEAM, m.PERMISSION_ADMIN, FULL_ACCESS)
+			sc.dashboardPermissionScenario(TEAM, m.PERMISSION_EDIT, FULL_ACCESS)
+			sc.dashboardPermissionScenario(TEAM, m.PERMISSION_VIEW, FULL_ACCESS)
+
+			// dashboard has editor role with permission
+			sc.dashboardPermissionScenario(EDITOR, m.PERMISSION_ADMIN, FULL_ACCESS)
+			sc.dashboardPermissionScenario(EDITOR, m.PERMISSION_EDIT, FULL_ACCESS)
+			sc.dashboardPermissionScenario(EDITOR, m.PERMISSION_VIEW, FULL_ACCESS)
+
+			// dashboard has viewer role with permission
+			sc.dashboardPermissionScenario(VIEWER, m.PERMISSION_ADMIN, FULL_ACCESS)
+			sc.dashboardPermissionScenario(VIEWER, m.PERMISSION_EDIT, FULL_ACCESS)
+			sc.dashboardPermissionScenario(VIEWER, m.PERMISSION_VIEW, FULL_ACCESS)
+
+			// parent folder has user with permission
+			sc.parentFolderPermissionScenario(USER, m.PERMISSION_ADMIN, FULL_ACCESS)
+			sc.parentFolderPermissionScenario(USER, m.PERMISSION_EDIT, FULL_ACCESS)
+			sc.parentFolderPermissionScenario(USER, m.PERMISSION_VIEW, FULL_ACCESS)
+
+			// parent folder has team with permission
+			sc.parentFolderPermissionScenario(TEAM, m.PERMISSION_ADMIN, FULL_ACCESS)
+			sc.parentFolderPermissionScenario(TEAM, m.PERMISSION_EDIT, FULL_ACCESS)
+			sc.parentFolderPermissionScenario(TEAM, m.PERMISSION_VIEW, FULL_ACCESS)
+
+			// parent folder has editor role with permission
+			sc.parentFolderPermissionScenario(EDITOR, m.PERMISSION_ADMIN, FULL_ACCESS)
+			sc.parentFolderPermissionScenario(EDITOR, m.PERMISSION_EDIT, FULL_ACCESS)
+			sc.parentFolderPermissionScenario(EDITOR, m.PERMISSION_VIEW, FULL_ACCESS)
+
+			// parent folder has viweer role with permission
+			sc.parentFolderPermissionScenario(VIEWER, m.PERMISSION_ADMIN, FULL_ACCESS)
+			sc.parentFolderPermissionScenario(VIEWER, m.PERMISSION_EDIT, FULL_ACCESS)
+			sc.parentFolderPermissionScenario(VIEWER, m.PERMISSION_VIEW, FULL_ACCESS)
+		})
+	})
+}
 
-				bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
-					query.Result = existingPermissions
-					return nil
-				})
-
-				Convey("When trying to update dashboard permissions with admin user permission should be allowed", func() {
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_ADMIN},
-					}
-					ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(ok, ShouldBeTrue)
-				})
-
-				Convey("When trying to update dashboard permissions with edit user permission should be allowed", func() {
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_EDIT},
-					}
-					ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(ok, ShouldBeTrue)
-				})
-
-				Convey("When trying to update dashboard permissions with view user permission should return error", func() {
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_VIEW},
-					}
-					_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(err, ShouldEqual, ErrGuardianOverride)
-				})
-			})
-
-			Convey("Given parent folder has team admin permission", func() {
-				existingPermissions := []*m.DashboardAclInfoDTO{
-					{OrgId: 1, DashboardId: 2, TeamId: 1, Permission: m.PERMISSION_ADMIN},
-				}
+func TestGuardianEditor(t *testing.T) {
+	Convey("Guardian editor org role tests", t, func() {
+		orgRoleScenario("Given user has editor org role", t, m.ROLE_EDITOR, func(sc *scenarioContext) {
+			// dashboard has user with permission
+			sc.dashboardPermissionScenario(USER, m.PERMISSION_ADMIN, FULL_ACCESS)
+			sc.dashboardPermissionScenario(USER, m.PERMISSION_EDIT, EDITOR_ACCESS)
+			sc.dashboardPermissionScenario(USER, m.PERMISSION_VIEW, CAN_VIEW)
+
+			// dashboard has team with permission
+			sc.dashboardPermissionScenario(TEAM, m.PERMISSION_ADMIN, FULL_ACCESS)
+			sc.dashboardPermissionScenario(TEAM, m.PERMISSION_EDIT, EDITOR_ACCESS)
+			sc.dashboardPermissionScenario(TEAM, m.PERMISSION_VIEW, CAN_VIEW)
+
+			// dashboard has editor role with permission
+			sc.dashboardPermissionScenario(EDITOR, m.PERMISSION_ADMIN, FULL_ACCESS)
+			sc.dashboardPermissionScenario(EDITOR, m.PERMISSION_EDIT, EDITOR_ACCESS)
+			sc.dashboardPermissionScenario(EDITOR, m.PERMISSION_VIEW, VIEWER_ACCESS)
+
+			// dashboard has viewer role with permission
+			sc.dashboardPermissionScenario(VIEWER, m.PERMISSION_ADMIN, NO_ACCESS)
+			sc.dashboardPermissionScenario(VIEWER, m.PERMISSION_EDIT, NO_ACCESS)
+			sc.dashboardPermissionScenario(VIEWER, m.PERMISSION_VIEW, NO_ACCESS)
+
+			// parent folder has user with permission
+			sc.parentFolderPermissionScenario(USER, m.PERMISSION_ADMIN, FULL_ACCESS)
+			sc.parentFolderPermissionScenario(USER, m.PERMISSION_EDIT, EDITOR_ACCESS)
+			sc.parentFolderPermissionScenario(USER, m.PERMISSION_VIEW, VIEWER_ACCESS)
+
+			// parent folder has team with permission
+			sc.parentFolderPermissionScenario(TEAM, m.PERMISSION_ADMIN, FULL_ACCESS)
+			sc.parentFolderPermissionScenario(TEAM, m.PERMISSION_EDIT, EDITOR_ACCESS)
+			sc.parentFolderPermissionScenario(TEAM, m.PERMISSION_VIEW, VIEWER_ACCESS)
+
+			// parent folder has editor role with permission
+			sc.parentFolderPermissionScenario(EDITOR, m.PERMISSION_ADMIN, FULL_ACCESS)
+			sc.parentFolderPermissionScenario(EDITOR, m.PERMISSION_EDIT, EDITOR_ACCESS)
+			sc.parentFolderPermissionScenario(EDITOR, m.PERMISSION_VIEW, VIEWER_ACCESS)
+
+			// parent folder has viweer role with permission
+			sc.parentFolderPermissionScenario(VIEWER, m.PERMISSION_ADMIN, NO_ACCESS)
+			sc.parentFolderPermissionScenario(VIEWER, m.PERMISSION_EDIT, NO_ACCESS)
+			sc.parentFolderPermissionScenario(VIEWER, m.PERMISSION_VIEW, NO_ACCESS)
+		})
+	})
+}
 
-				bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
-					query.Result = existingPermissions
-					return nil
-				})
-
-				Convey("When trying to update dashboard permissions with admin team permission should return error", func() {
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_ADMIN},
-					}
-					_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(err, ShouldEqual, ErrGuardianOverride)
-				})
-
-				Convey("When trying to update dashboard permissions with edit team permission should return error", func() {
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_EDIT},
-					}
-					_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(err, ShouldEqual, ErrGuardianOverride)
-				})
-
-				Convey("When trying to update dashboard permissions with view team permission should return error", func() {
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_VIEW},
-					}
-					_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(err, ShouldEqual, ErrGuardianOverride)
-				})
-			})
-
-			Convey("Given parent folder has team edit permission", func() {
-				existingPermissions := []*m.DashboardAclInfoDTO{
-					{OrgId: 1, DashboardId: 2, TeamId: 1, Permission: m.PERMISSION_EDIT},
-				}
+func TestGuardianViewer(t *testing.T) {
+	Convey("Guardian viewer org role tests", t, func() {
+		orgRoleScenario("Given user has viewer org role", t, m.ROLE_VIEWER, func(sc *scenarioContext) {
+			// dashboard has user with permission
+			sc.dashboardPermissionScenario(USER, m.PERMISSION_ADMIN, FULL_ACCESS)
+			sc.dashboardPermissionScenario(USER, m.PERMISSION_EDIT, EDITOR_ACCESS)
+			sc.dashboardPermissionScenario(USER, m.PERMISSION_VIEW, VIEWER_ACCESS)
+
+			// dashboard has team with permission
+			sc.dashboardPermissionScenario(TEAM, m.PERMISSION_ADMIN, FULL_ACCESS)
+			sc.dashboardPermissionScenario(TEAM, m.PERMISSION_EDIT, EDITOR_ACCESS)
+			sc.dashboardPermissionScenario(TEAM, m.PERMISSION_VIEW, VIEWER_ACCESS)
+
+			// dashboard has editor role with permission
+			sc.dashboardPermissionScenario(EDITOR, m.PERMISSION_ADMIN, NO_ACCESS)
+			sc.dashboardPermissionScenario(EDITOR, m.PERMISSION_EDIT, NO_ACCESS)
+			sc.dashboardPermissionScenario(EDITOR, m.PERMISSION_VIEW, NO_ACCESS)
+
+			// dashboard has viewer role with permission
+			sc.dashboardPermissionScenario(VIEWER, m.PERMISSION_ADMIN, FULL_ACCESS)
+			sc.dashboardPermissionScenario(VIEWER, m.PERMISSION_EDIT, EDITOR_ACCESS)
+			sc.dashboardPermissionScenario(VIEWER, m.PERMISSION_VIEW, VIEWER_ACCESS)
+
+			// parent folder has user with permission
+			sc.parentFolderPermissionScenario(USER, m.PERMISSION_ADMIN, FULL_ACCESS)
+			sc.parentFolderPermissionScenario(USER, m.PERMISSION_EDIT, EDITOR_ACCESS)
+			sc.parentFolderPermissionScenario(USER, m.PERMISSION_VIEW, VIEWER_ACCESS)
+
+			// parent folder has team with permission
+			sc.parentFolderPermissionScenario(TEAM, m.PERMISSION_ADMIN, FULL_ACCESS)
+			sc.parentFolderPermissionScenario(TEAM, m.PERMISSION_EDIT, EDITOR_ACCESS)
+			sc.parentFolderPermissionScenario(TEAM, m.PERMISSION_VIEW, VIEWER_ACCESS)
+
+			// parent folder has editor role with permission
+			sc.parentFolderPermissionScenario(EDITOR, m.PERMISSION_ADMIN, NO_ACCESS)
+			sc.parentFolderPermissionScenario(EDITOR, m.PERMISSION_EDIT, NO_ACCESS)
+			sc.parentFolderPermissionScenario(EDITOR, m.PERMISSION_VIEW, NO_ACCESS)
+
+			// parent folder has viweer role with permission
+			sc.parentFolderPermissionScenario(VIEWER, m.PERMISSION_ADMIN, FULL_ACCESS)
+			sc.parentFolderPermissionScenario(VIEWER, m.PERMISSION_EDIT, EDITOR_ACCESS)
+			sc.parentFolderPermissionScenario(VIEWER, m.PERMISSION_VIEW, VIEWER_ACCESS)
+		})
+	})
+}
 
-				bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
-					query.Result = existingPermissions
-					return nil
-				})
-
-				Convey("When trying to update dashboard permissions with admin team permission should be allowed", func() {
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_ADMIN},
-					}
-					ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(ok, ShouldBeTrue)
-				})
-
-				Convey("When trying to update dashboard permissions with edit team permission should return error", func() {
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_EDIT},
-					}
-					_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(err, ShouldEqual, ErrGuardianOverride)
-				})
-
-				Convey("When trying to update dashboard permissions with view team permission should return error", func() {
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_VIEW},
-					}
-					_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(err, ShouldEqual, ErrGuardianOverride)
-				})
-			})
-
-			Convey("Given parent folder has team view permission", func() {
-				existingPermissions := []*m.DashboardAclInfoDTO{
-					{OrgId: 1, DashboardId: 2, TeamId: 1, Permission: m.PERMISSION_VIEW},
-				}
+func (sc *scenarioContext) defaultPermissionScenario(pt permissionType, permission m.PermissionType, flag permissionFlags) {
+	_, callerFile, callerLine, _ := runtime.Caller(1)
+	sc.callerFile = callerFile
+	sc.callerLine = callerLine
+	existingPermissions := []*m.DashboardAclInfoDTO{
+		toDto(newEditorRolePermission(defaultDashboardID, m.PERMISSION_EDIT)),
+		toDto(newViewerRolePermission(defaultDashboardID, m.PERMISSION_VIEW)),
+	}
 
-				bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
-					query.Result = existingPermissions
-					return nil
-				})
-
-				Convey("When trying to update dashboard permissions with admin team permission should be allowed", func() {
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_ADMIN},
-					}
-					ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(ok, ShouldBeTrue)
-				})
-
-				Convey("When trying to update dashboard permissions with edit team permission should be allowed", func() {
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_EDIT},
-					}
-					ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(ok, ShouldBeTrue)
-				})
-
-				Convey("When trying to update dashboard permissions with view team permission should return error", func() {
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_VIEW},
-					}
-					_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(err, ShouldEqual, ErrGuardianOverride)
-				})
-			})
-
-			Convey("Given parent folder has editor role with edit permission", func() {
-				r := m.ROLE_EDITOR
-				existingPermissions := []*m.DashboardAclInfoDTO{
-					{OrgId: 1, DashboardId: 2, Role: &r, Permission: m.PERMISSION_EDIT},
-				}
+	permissionScenario("and existing permissions is the default permissions (everyone with editor role can edit, everyone with viewer role can view)", dashboardID, sc, existingPermissions, func(sc *scenarioContext) {
+		sc.expectedFlags = flag
+		sc.verifyExpectedPermissionsFlags()
+		sc.verifyDuplicatePermissionsShouldNotBeAllowed()
+		sc.verifyUpdateDashboardPermissionsShouldBeAllowed(pt)
+		sc.verifyUpdateDashboardPermissionsShouldNotBeAllowed(pt)
+	})
+}
 
-				bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
-					query.Result = existingPermissions
-					return nil
-				})
-
-				Convey("When trying to update dashboard permissions with everyone with editor role can admin permission should be allowed", func() {
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_ADMIN},
-					}
-					ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(ok, ShouldBeTrue)
-				})
-
-				Convey("When trying to update dashboard permissions with everyone with editor role can edit permission should return error", func() {
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_EDIT},
-					}
-					_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(err, ShouldEqual, ErrGuardianOverride)
-				})
-
-				Convey("When trying to update dashboard permissions with everyone with editor role can view permission should return error", func() {
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_VIEW},
-					}
-					_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(err, ShouldEqual, ErrGuardianOverride)
-				})
-			})
-
-			Convey("Given parent folder has editor role with view permission", func() {
-				r := m.ROLE_EDITOR
-				existingPermissions := []*m.DashboardAclInfoDTO{
-					{OrgId: 1, DashboardId: 2, Role: &r, Permission: m.PERMISSION_VIEW},
-				}
+func (sc *scenarioContext) dashboardPermissionScenario(pt permissionType, permission m.PermissionType, flag permissionFlags) {
+	_, callerFile, callerLine, _ := runtime.Caller(1)
+	sc.callerFile = callerFile
+	sc.callerLine = callerLine
+	var existingPermissions []*m.DashboardAclInfoDTO
+
+	switch pt {
+	case USER:
+		existingPermissions = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: dashboardID, UserId: userID, Permission: permission}}
+	case TEAM:
+		existingPermissions = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: dashboardID, TeamId: teamID, Permission: permission}}
+	case EDITOR:
+		existingPermissions = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: dashboardID, Role: &editorRole, Permission: permission}}
+	case VIEWER:
+		existingPermissions = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: dashboardID, Role: &viewerRole, Permission: permission}}
+	}
 
-				bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
-					query.Result = existingPermissions
-					return nil
-				})
-
-				Convey("When trying to update dashboard permissions with everyone with viewer role can admin permission should be allowed", func() {
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_ADMIN},
-					}
-					ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(ok, ShouldBeTrue)
-				})
-
-				Convey("When trying to update dashboard permissions with everyone with viewer role can edit permission should be allowed", func() {
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_EDIT},
-					}
-					ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(ok, ShouldBeTrue)
-				})
-
-				Convey("When trying to update dashboard permissions with everyone with viewer role can view permission should return error", func() {
-					p := []*m.DashboardAcl{
-						{OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_VIEW},
-					}
-					_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-					So(err, ShouldEqual, ErrGuardianOverride)
-				})
-			})
-		})
+	permissionScenario(fmt.Sprintf("and %s has permission to %s dashboard", pt.String(), permission.String()), dashboardID, sc, existingPermissions, func(sc *scenarioContext) {
+		sc.expectedFlags = flag
+		sc.verifyExpectedPermissionsFlags()
+		sc.verifyDuplicatePermissionsShouldNotBeAllowed()
+		sc.verifyUpdateDashboardPermissionsShouldBeAllowed(pt)
+		sc.verifyUpdateDashboardPermissionsShouldNotBeAllowed(pt)
+	})
+}
 
-		orgRoleScenario("Given user has editor org role", m.ROLE_EDITOR, func(sc *scenarioContext) {
-			everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
-				canAdmin, _ := sc.g.CanAdmin()
-				canEdit, _ := sc.g.CanEdit()
-				canSave, _ := sc.g.CanSave()
-				canView, _ := sc.g.CanView()
-				So(canAdmin, ShouldBeTrue)
-				So(canEdit, ShouldBeTrue)
-				So(canSave, ShouldBeTrue)
-				So(canView, ShouldBeTrue)
-			})
-
-			everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
-				canAdmin, _ := sc.g.CanAdmin()
-				canEdit, _ := sc.g.CanEdit()
-				canSave, _ := sc.g.CanSave()
-				canView, _ := sc.g.CanView()
-				So(canAdmin, ShouldBeFalse)
-				So(canEdit, ShouldBeTrue)
-				So(canSave, ShouldBeTrue)
-				So(canView, ShouldBeTrue)
-			})
-
-			everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
-				canAdmin, _ := sc.g.CanAdmin()
-				canEdit, _ := sc.g.CanEdit()
-				canSave, _ := sc.g.CanSave()
-				canView, _ := sc.g.CanView()
-				So(canAdmin, ShouldBeFalse)
-				So(canEdit, ShouldBeFalse)
-				So(canSave, ShouldBeFalse)
-				So(canView, ShouldBeTrue)
-			})
-
-			everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
-				canAdmin, _ := sc.g.CanAdmin()
-				canEdit, _ := sc.g.CanEdit()
-				canSave, _ := sc.g.CanSave()
-				canView, _ := sc.g.CanView()
-				So(canAdmin, ShouldBeFalse)
-				So(canEdit, ShouldBeFalse)
-				So(canSave, ShouldBeFalse)
-				So(canView, ShouldBeFalse)
-			})
-
-			everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
-				canAdmin, _ := sc.g.CanAdmin()
-				canEdit, _ := sc.g.CanEdit()
-				canSave, _ := sc.g.CanSave()
-				canView, _ := sc.g.CanView()
-				So(canAdmin, ShouldBeFalse)
-				So(canEdit, ShouldBeFalse)
-				So(canSave, ShouldBeFalse)
-				So(canView, ShouldBeFalse)
-			})
-
-			everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
-				canAdmin, _ := sc.g.CanAdmin()
-				canEdit, _ := sc.g.CanEdit()
-				canSave, _ := sc.g.CanSave()
-				canView, _ := sc.g.CanView()
-				So(canAdmin, ShouldBeFalse)
-				So(canEdit, ShouldBeFalse)
-				So(canSave, ShouldBeFalse)
-				So(canView, ShouldBeFalse)
-			})
-
-			userWithPermissionScenario(m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
-				canAdmin, _ := sc.g.CanAdmin()
-				canEdit, _ := sc.g.CanEdit()
-				canSave, _ := sc.g.CanSave()
-				canView, _ := sc.g.CanView()
-				So(canAdmin, ShouldBeTrue)
-				So(canEdit, ShouldBeTrue)
-				So(canSave, ShouldBeTrue)
-				So(canView, ShouldBeTrue)
-			})
-
-			userWithPermissionScenario(m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
-				canAdmin, _ := sc.g.CanAdmin()
-				canEdit, _ := sc.g.CanEdit()
-				canSave, _ := sc.g.CanSave()
-				canView, _ := sc.g.CanView()
-				So(canAdmin, ShouldBeFalse)
-				So(canEdit, ShouldBeTrue)
-				So(canSave, ShouldBeTrue)
-				So(canView, ShouldBeTrue)
-			})
-
-			userWithPermissionScenario(m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
-				canAdmin, _ := sc.g.CanAdmin()
-				canEdit, _ := sc.g.CanEdit()
-				canSave, _ := sc.g.CanSave()
-				canView, _ := sc.g.CanView()
-				So(canAdmin, ShouldBeFalse)
-				So(canEdit, ShouldBeFalse)
-				So(canSave, ShouldBeFalse)
-				So(canView, ShouldBeTrue)
-			})
-
-			teamWithPermissionScenario(m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
-				canAdmin, _ := sc.g.CanAdmin()
-				canEdit, _ := sc.g.CanEdit()
-				canSave, _ := sc.g.CanSave()
-				canView, _ := sc.g.CanView()
-				So(canAdmin, ShouldBeTrue)
-				So(canEdit, ShouldBeTrue)
-				So(canSave, ShouldBeTrue)
-				So(canView, ShouldBeTrue)
-			})
-
-			teamWithPermissionScenario(m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
-				canAdmin, _ := sc.g.CanAdmin()
-				canEdit, _ := sc.g.CanEdit()
-				canSave, _ := sc.g.CanSave()
-				canView, _ := sc.g.CanView()
-				So(canAdmin, ShouldBeFalse)
-				So(canEdit, ShouldBeTrue)
-				So(canSave, ShouldBeTrue)
-				So(canView, ShouldBeTrue)
-			})
-
-			teamWithPermissionScenario(m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
-				canAdmin, _ := sc.g.CanAdmin()
-				canEdit, _ := sc.g.CanEdit()
-				canSave, _ := sc.g.CanSave()
-				canView, _ := sc.g.CanView()
-				So(canAdmin, ShouldBeFalse)
-				So(canEdit, ShouldBeFalse)
-				So(canSave, ShouldBeFalse)
-				So(canView, ShouldBeTrue)
-			})
-
-			Convey("When trying to update permissions should return false", func() {
-				p := []*m.DashboardAcl{
-					{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_VIEW},
-					{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN},
-				}
-				ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-				So(ok, ShouldBeFalse)
-			})
-		})
+func (sc *scenarioContext) parentFolderPermissionScenario(pt permissionType, permission m.PermissionType, flag permissionFlags) {
+	_, callerFile, callerLine, _ := runtime.Caller(1)
+	sc.callerFile = callerFile
+	sc.callerLine = callerLine
+	var folderPermissionList []*m.DashboardAclInfoDTO
+
+	switch pt {
+	case USER:
+		folderPermissionList = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: parentFolderID, UserId: userID, Permission: permission}}
+	case TEAM:
+		folderPermissionList = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: parentFolderID, TeamId: teamID, Permission: permission}}
+	case EDITOR:
+		folderPermissionList = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: parentFolderID, Role: &editorRole, Permission: permission}}
+	case VIEWER:
+		folderPermissionList = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: parentFolderID, Role: &viewerRole, Permission: permission}}
+	}
 
-		orgRoleScenario("Given user has viewer org role", m.ROLE_VIEWER, func(sc *scenarioContext) {
-			everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
-				canAdmin, _ := sc.g.CanAdmin()
-				canEdit, _ := sc.g.CanEdit()
-				canSave, _ := sc.g.CanSave()
-				canView, _ := sc.g.CanView()
-				So(canAdmin, ShouldBeFalse)
-				So(canEdit, ShouldBeFalse)
-				So(canSave, ShouldBeFalse)
-				So(canView, ShouldBeFalse)
-			})
-
-			everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
-				canAdmin, _ := sc.g.CanAdmin()
-				canEdit, _ := sc.g.CanEdit()
-				canSave, _ := sc.g.CanSave()
-				canView, _ := sc.g.CanView()
-				So(canAdmin, ShouldBeFalse)
-				So(canEdit, ShouldBeFalse)
-				So(canSave, ShouldBeFalse)
-				So(canView, ShouldBeFalse)
-			})
-
-			everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
-				canAdmin, _ := sc.g.CanAdmin()
-				canEdit, _ := sc.g.CanEdit()
-				canSave, _ := sc.g.CanSave()
-				canView, _ := sc.g.CanView()
-				So(canAdmin, ShouldBeFalse)
-				So(canEdit, ShouldBeFalse)
-				So(canSave, ShouldBeFalse)
-				So(canView, ShouldBeFalse)
-			})
-
-			everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
-				canAdmin, _ := sc.g.CanAdmin()
-				canEdit, _ := sc.g.CanEdit()
-				canSave, _ := sc.g.CanSave()
-				canView, _ := sc.g.CanView()
-				So(canAdmin, ShouldBeTrue)
-				So(canEdit, ShouldBeTrue)
-				So(canSave, ShouldBeTrue)
-				So(canView, ShouldBeTrue)
-			})
-
-			everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
-				canAdmin, _ := sc.g.CanAdmin()
-				canEdit, _ := sc.g.CanEdit()
-				canSave, _ := sc.g.CanSave()
-				canView, _ := sc.g.CanView()
-				So(canAdmin, ShouldBeFalse)
-				So(canEdit, ShouldBeTrue)
-				So(canSave, ShouldBeTrue)
-				So(canView, ShouldBeTrue)
-			})
-
-			everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
-				canAdmin, _ := sc.g.CanAdmin()
-				canEdit, _ := sc.g.CanEdit()
-				canSave, _ := sc.g.CanSave()
-				canView, _ := sc.g.CanView()
-				So(canAdmin, ShouldBeFalse)
-				So(canEdit, ShouldBeFalse)
-				So(canSave, ShouldBeFalse)
-				So(canView, ShouldBeTrue)
-			})
-
-			userWithPermissionScenario(m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
-				canAdmin, _ := sc.g.CanAdmin()
-				canEdit, _ := sc.g.CanEdit()
-				canSave, _ := sc.g.CanSave()
-				canView, _ := sc.g.CanView()
-				So(canAdmin, ShouldBeTrue)
-				So(canEdit, ShouldBeTrue)
-				So(canSave, ShouldBeTrue)
-				So(canView, ShouldBeTrue)
-			})
-
-			userWithPermissionScenario(m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
-				canAdmin, _ := sc.g.CanAdmin()
-				canEdit, _ := sc.g.CanEdit()
-				canSave, _ := sc.g.CanSave()
-				canView, _ := sc.g.CanView()
-				So(canAdmin, ShouldBeFalse)
-				So(canEdit, ShouldBeTrue)
-				So(canSave, ShouldBeTrue)
-				So(canView, ShouldBeTrue)
-			})
-
-			userWithPermissionScenario(m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
-				canAdmin, _ := sc.g.CanAdmin()
-				canEdit, _ := sc.g.CanEdit()
-				canSave, _ := sc.g.CanSave()
-				canView, _ := sc.g.CanView()
-				So(canAdmin, ShouldBeFalse)
-				So(canEdit, ShouldBeFalse)
-				So(canSave, ShouldBeFalse)
-				So(canView, ShouldBeTrue)
-			})
-
-			Convey("When trying to update permissions should return false", func() {
-				p := []*m.DashboardAcl{
-					{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_VIEW},
-					{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN},
-				}
-				ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
-				So(ok, ShouldBeFalse)
-			})
-		})
+	permissionScenario(fmt.Sprintf("and parent folder has %s with permission to %s", pt.String(), permission.String()), childDashboardID, sc, folderPermissionList, func(sc *scenarioContext) {
+		sc.expectedFlags = flag
+		sc.verifyExpectedPermissionsFlags()
+		sc.verifyDuplicatePermissionsShouldNotBeAllowed()
+		sc.verifyUpdateChildDashboardPermissionsShouldBeAllowed(pt, permission)
+		sc.verifyUpdateChildDashboardPermissionsShouldNotBeAllowed(pt, permission)
+		sc.verifyUpdateChildDashboardPermissionsWithOverrideShouldBeAllowed(pt, permission)
+		sc.verifyUpdateChildDashboardPermissionsWithOverrideShouldNotBeAllowed(pt, permission)
 	})
 }
 
-type scenarioContext struct {
-	g DashboardGuardian
-}
+func (sc *scenarioContext) verifyExpectedPermissionsFlags() {
+	canAdmin, _ := sc.g.CanAdmin()
+	canEdit, _ := sc.g.CanEdit()
+	canSave, _ := sc.g.CanSave()
+	canView, _ := sc.g.CanView()
 
-type scenarioFunc func(c *scenarioContext)
+	tc := fmt.Sprintf("should have permissions to %s", sc.expectedFlags.String())
+	Convey(tc, func() {
+		var actualFlag permissionFlags
 
-func orgRoleScenario(desc string, role m.RoleType, fn scenarioFunc) {
-	user := &m.SignedInUser{
-		UserId:  1,
-		OrgId:   1,
-		OrgRole: role,
-	}
-	guard := New(1, 1, user)
-	sc := &scenarioContext{
-		g: guard,
-	}
+		if canAdmin {
+			actualFlag |= CAN_ADMIN
+		}
+
+		if canEdit {
+			actualFlag |= CAN_EDIT
+		}
+
+		if canSave {
+			actualFlag |= CAN_SAVE
+		}
+
+		if canView {
+			actualFlag |= CAN_VIEW
+		}
+
+		if actualFlag.noAccess() {
+			actualFlag = NO_ACCESS
+		}
+
+		if sc.expectedFlags&actualFlag != sc.expectedFlags {
+			sc.reportFailure(tc, sc.expectedFlags.String(), actualFlag.String())
+		}
 
-	Convey(desc, func() {
-		fn(sc)
+		sc.reportSuccess()
 	})
 }
 
-func permissionScenario(desc string, sc *scenarioContext, permissions []*m.DashboardAclInfoDTO, fn scenarioFunc) {
-	bus.ClearBusHandlers()
+func (sc *scenarioContext) verifyDuplicatePermissionsShouldNotBeAllowed() {
+	if !sc.expectedFlags.canAdmin() {
+		return
+	}
+
+	tc := "When updating dashboard permissions with duplicate permission for user should not be allowed"
+	Convey(tc, func() {
+		p := []*m.DashboardAcl{
+			newDefaultUserPermission(dashboardID, m.PERMISSION_VIEW),
+			newDefaultUserPermission(dashboardID, m.PERMISSION_ADMIN),
+		}
+		sc.updatePermissions = p
+		_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
 
-	bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
-		query.Result = permissions
-		return nil
+		if err != ErrGuardianPermissionExists {
+			sc.reportFailure(tc, ErrGuardianPermissionExists, err)
+		}
+		sc.reportSuccess()
 	})
 
-	teams := []*m.Team{}
+	tc = "When updating dashboard permissions with duplicate permission for team should not be allowed"
+	Convey(tc, func() {
+		p := []*m.DashboardAcl{
+			newDefaultTeamPermission(dashboardID, m.PERMISSION_VIEW),
+			newDefaultTeamPermission(dashboardID, m.PERMISSION_ADMIN),
+		}
+		sc.updatePermissions = p
+		_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
 
-	for _, p := range permissions {
-		if p.TeamId > 0 {
-			teams = append(teams, &m.Team{Id: p.TeamId})
+		if err != ErrGuardianPermissionExists {
+			sc.reportFailure(tc, ErrGuardianPermissionExists, err)
 		}
-	}
+		sc.reportSuccess()
+	})
+
+	tc = "When updating dashboard permissions with duplicate permission for editor role should not be allowed"
+	Convey(tc, func() {
+		p := []*m.DashboardAcl{
+			newEditorRolePermission(dashboardID, m.PERMISSION_VIEW),
+			newEditorRolePermission(dashboardID, m.PERMISSION_ADMIN),
+		}
+		sc.updatePermissions = p
+		_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+
+		if err != ErrGuardianPermissionExists {
+			sc.reportFailure(tc, ErrGuardianPermissionExists, err)
+		}
+		sc.reportSuccess()
+	})
 
-	bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
-		query.Result = teams
-		return nil
+	tc = "When updating dashboard permissions with duplicate permission for viewer role should not be allowed"
+	Convey(tc, func() {
+		p := []*m.DashboardAcl{
+			newViewerRolePermission(dashboardID, m.PERMISSION_VIEW),
+			newViewerRolePermission(dashboardID, m.PERMISSION_ADMIN),
+		}
+		sc.updatePermissions = p
+		_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+
+		if err != ErrGuardianPermissionExists {
+			sc.reportFailure(tc, ErrGuardianPermissionExists, err)
+		}
+		sc.reportSuccess()
 	})
 
-	Convey(desc, func() {
-		fn(sc)
+	tc = "When updating dashboard permissions with duplicate permission for admin role should not be allowed"
+	Convey(tc, func() {
+		p := []*m.DashboardAcl{
+			newAdminRolePermission(dashboardID, m.PERMISSION_ADMIN),
+		}
+		sc.updatePermissions = p
+		_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+
+		if err != ErrGuardianPermissionExists {
+			sc.reportFailure(tc, ErrGuardianPermissionExists, err)
+		}
+		sc.reportSuccess()
 	})
 }
 
-func userWithPermissionScenario(permission m.PermissionType, sc *scenarioContext, fn scenarioFunc) {
-	p := []*m.DashboardAclInfoDTO{
-		{OrgId: 1, DashboardId: 1, UserId: 1, Permission: permission},
+func (sc *scenarioContext) verifyUpdateDashboardPermissionsShouldBeAllowed(pt permissionType) {
+	if !sc.expectedFlags.canAdmin() {
+		return
+	}
+
+	for _, p := range []m.PermissionType{m.PERMISSION_ADMIN, m.PERMISSION_EDIT, m.PERMISSION_VIEW} {
+		tc := fmt.Sprintf("When updating dashboard permissions with %s permissions should be allowed", p.String())
+
+		Convey(tc, func() {
+			permissionList := []*m.DashboardAcl{}
+			switch pt {
+			case USER:
+				permissionList = []*m.DashboardAcl{
+					newEditorRolePermission(dashboardID, p),
+					newViewerRolePermission(dashboardID, p),
+					newCustomUserPermission(dashboardID, otherUserID, p),
+					newDefaultTeamPermission(dashboardID, p),
+				}
+			case TEAM:
+				permissionList = []*m.DashboardAcl{
+					newEditorRolePermission(dashboardID, p),
+					newViewerRolePermission(dashboardID, p),
+					newDefaultUserPermission(dashboardID, p),
+					newCustomTeamPermission(dashboardID, otherTeamID, p),
+				}
+			case EDITOR, VIEWER:
+				permissionList = []*m.DashboardAcl{
+					newEditorRolePermission(dashboardID, p),
+					newViewerRolePermission(dashboardID, p),
+					newDefaultUserPermission(dashboardID, p),
+					newDefaultTeamPermission(dashboardID, p),
+				}
+			}
+
+			sc.updatePermissions = permissionList
+			ok, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, permissionList)
+
+			if err != nil {
+				sc.reportFailure(tc, nil, err)
+			}
+			if !ok {
+				sc.reportFailure(tc, false, true)
+			}
+			sc.reportSuccess()
+		})
+	}
+}
+
+func (sc *scenarioContext) verifyUpdateDashboardPermissionsShouldNotBeAllowed(pt permissionType) {
+	if sc.expectedFlags.canAdmin() {
+		return
+	}
+
+	for _, p := range []m.PermissionType{m.PERMISSION_ADMIN, m.PERMISSION_EDIT, m.PERMISSION_VIEW} {
+		tc := fmt.Sprintf("When updating dashboard permissions with %s permissions should NOT be allowed", p.String())
+
+		Convey(tc, func() {
+			permissionList := []*m.DashboardAcl{
+				newEditorRolePermission(dashboardID, p),
+				newViewerRolePermission(dashboardID, p),
+			}
+			switch pt {
+			case USER:
+				permissionList = append(permissionList, []*m.DashboardAcl{
+					newCustomUserPermission(dashboardID, otherUserID, p),
+					newDefaultTeamPermission(dashboardID, p),
+				}...)
+			case TEAM:
+				permissionList = append(permissionList, []*m.DashboardAcl{
+					newDefaultUserPermission(dashboardID, p),
+					newCustomTeamPermission(dashboardID, otherTeamID, p),
+				}...)
+			}
+
+			sc.updatePermissions = permissionList
+			ok, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, permissionList)
+
+			if err != nil {
+				sc.reportFailure(tc, nil, err)
+			}
+			if ok {
+				sc.reportFailure(tc, true, false)
+			}
+			sc.reportSuccess()
+		})
+	}
+}
+
+func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsShouldBeAllowed(pt permissionType, parentFolderPermission m.PermissionType) {
+	if !sc.expectedFlags.canAdmin() {
+		return
+	}
+
+	for _, p := range []m.PermissionType{m.PERMISSION_ADMIN, m.PERMISSION_EDIT, m.PERMISSION_VIEW} {
+		tc := fmt.Sprintf("When updating child dashboard permissions with %s permissions should be allowed", p.String())
+
+		Convey(tc, func() {
+			permissionList := []*m.DashboardAcl{}
+			switch pt {
+			case USER:
+				permissionList = []*m.DashboardAcl{
+					newEditorRolePermission(childDashboardID, p),
+					newViewerRolePermission(childDashboardID, p),
+					newCustomUserPermission(childDashboardID, otherUserID, p),
+					newDefaultTeamPermission(childDashboardID, p),
+				}
+			case TEAM:
+				permissionList = []*m.DashboardAcl{
+					newEditorRolePermission(childDashboardID, p),
+					newViewerRolePermission(childDashboardID, p),
+					newDefaultUserPermission(childDashboardID, p),
+					newCustomTeamPermission(childDashboardID, otherTeamID, p),
+				}
+			case EDITOR:
+				permissionList = []*m.DashboardAcl{
+					newViewerRolePermission(childDashboardID, p),
+					newDefaultUserPermission(childDashboardID, p),
+					newDefaultTeamPermission(childDashboardID, p),
+				}
+
+				// permission to update is higher than parent folder permission
+				if p > parentFolderPermission {
+					permissionList = append(permissionList, newEditorRolePermission(childDashboardID, p))
+				}
+			case VIEWER:
+				permissionList = []*m.DashboardAcl{
+					newEditorRolePermission(childDashboardID, p),
+					newDefaultUserPermission(childDashboardID, p),
+					newDefaultTeamPermission(childDashboardID, p),
+				}
+
+				// permission to update is higher than parent folder permission
+				if p > parentFolderPermission {
+					permissionList = append(permissionList, newViewerRolePermission(childDashboardID, p))
+				}
+			}
+
+			sc.updatePermissions = permissionList
+			ok, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, permissionList)
+
+			if err != nil {
+				sc.reportFailure(tc, nil, err)
+			}
+			if !ok {
+				sc.reportFailure(tc, false, true)
+			}
+			sc.reportSuccess()
+		})
+	}
+}
+
+func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsShouldNotBeAllowed(pt permissionType, parentFolderPermission m.PermissionType) {
+	if sc.expectedFlags.canAdmin() {
+		return
+	}
+
+	for _, p := range []m.PermissionType{m.PERMISSION_ADMIN, m.PERMISSION_EDIT, m.PERMISSION_VIEW} {
+		tc := fmt.Sprintf("When updating child dashboard permissions with %s permissions should NOT be allowed", p.String())
+
+		Convey(tc, func() {
+			permissionList := []*m.DashboardAcl{}
+			switch pt {
+			case USER:
+				permissionList = []*m.DashboardAcl{
+					newEditorRolePermission(childDashboardID, p),
+					newViewerRolePermission(childDashboardID, p),
+					newCustomUserPermission(childDashboardID, otherUserID, p),
+					newDefaultTeamPermission(childDashboardID, p),
+				}
+			case TEAM:
+				permissionList = []*m.DashboardAcl{
+					newEditorRolePermission(childDashboardID, p),
+					newViewerRolePermission(childDashboardID, p),
+					newDefaultUserPermission(childDashboardID, p),
+					newCustomTeamPermission(childDashboardID, otherTeamID, p),
+				}
+			case EDITOR:
+				permissionList = []*m.DashboardAcl{
+					newViewerRolePermission(childDashboardID, p),
+					newDefaultUserPermission(childDashboardID, p),
+					newDefaultTeamPermission(childDashboardID, p),
+				}
+
+				// perminssion to update is higher than parent folder permission
+				if p > parentFolderPermission {
+					permissionList = append(permissionList, newEditorRolePermission(childDashboardID, p))
+				}
+			case VIEWER:
+				permissionList = []*m.DashboardAcl{
+					newEditorRolePermission(childDashboardID, p),
+					newDefaultUserPermission(childDashboardID, p),
+					newDefaultTeamPermission(childDashboardID, p),
+				}
+
+				// perminssion to update is higher than parent folder permission
+				if p > parentFolderPermission {
+					permissionList = append(permissionList, newViewerRolePermission(childDashboardID, p))
+				}
+			}
+
+			sc.updatePermissions = permissionList
+			ok, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, permissionList)
+
+			if err != nil {
+				sc.reportFailure(tc, nil, err)
+			}
+			if ok {
+				sc.reportFailure(tc, true, false)
+			}
+			sc.reportSuccess()
+		})
 	}
-	permissionScenario(fmt.Sprintf("and user has permission to %s item", permission), sc, p, fn)
 }
 
-func teamWithPermissionScenario(permission m.PermissionType, sc *scenarioContext, fn scenarioFunc) {
-	p := []*m.DashboardAclInfoDTO{
-		{OrgId: 1, DashboardId: 1, TeamId: 1, Permission: permission},
+func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsWithOverrideShouldBeAllowed(pt permissionType, parentFolderPermission m.PermissionType) {
+	if !sc.expectedFlags.canAdmin() {
+		return
+	}
+
+	for _, p := range []m.PermissionType{m.PERMISSION_ADMIN, m.PERMISSION_EDIT, m.PERMISSION_VIEW} {
+		// perminssion to update is higher tban parent folder permission
+		if p > parentFolderPermission {
+			continue
+		}
+
+		tc := fmt.Sprintf("When updating child dashboard permissions overriding parent %s permission with %s permission should NOT be allowed", pt.String(), p.String())
+
+		Convey(tc, func() {
+			permissionList := []*m.DashboardAcl{}
+			switch pt {
+			case USER:
+				permissionList = []*m.DashboardAcl{
+					newDefaultUserPermission(childDashboardID, p),
+				}
+			case TEAM:
+				permissionList = []*m.DashboardAcl{
+					newDefaultTeamPermission(childDashboardID, p),
+				}
+			case EDITOR:
+				permissionList = []*m.DashboardAcl{
+					newEditorRolePermission(childDashboardID, p),
+				}
+			case VIEWER:
+				permissionList = []*m.DashboardAcl{
+					newViewerRolePermission(childDashboardID, p),
+				}
+			}
+
+			sc.updatePermissions = permissionList
+			_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, permissionList)
+
+			if err != ErrGuardianOverride {
+				sc.reportFailure(tc, ErrGuardianOverride, err)
+			}
+			sc.reportSuccess()
+		})
 	}
-	permissionScenario(fmt.Sprintf("and team has permission to %s item", permission), sc, p, fn)
 }
 
-func everyoneWithRoleScenario(role m.RoleType, permission m.PermissionType, sc *scenarioContext, fn scenarioFunc) {
-	p := []*m.DashboardAclInfoDTO{
-		{OrgId: 1, DashboardId: 1, UserId: -1, Role: &role, Permission: permission},
+func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsWithOverrideShouldNotBeAllowed(pt permissionType, parentFolderPermission m.PermissionType) {
+	if !sc.expectedFlags.canAdmin() {
+		return
+	}
+
+	for _, p := range []m.PermissionType{m.PERMISSION_ADMIN, m.PERMISSION_EDIT, m.PERMISSION_VIEW} {
+		// perminssion to update is lower than/equal parent folder permission
+		if p <= parentFolderPermission {
+			continue
+		}
+
+		tc := fmt.Sprintf("When updating child dashboard permissions overriding parent %s permission with %s permission should be allowed", pt.String(), p.String())
+
+		Convey(tc, func() {
+			permissionList := []*m.DashboardAcl{}
+			switch pt {
+			case USER:
+				permissionList = []*m.DashboardAcl{
+					newDefaultUserPermission(childDashboardID, p),
+				}
+			case TEAM:
+				permissionList = []*m.DashboardAcl{
+					newDefaultTeamPermission(childDashboardID, p),
+				}
+			case EDITOR:
+				permissionList = []*m.DashboardAcl{
+					newEditorRolePermission(childDashboardID, p),
+				}
+			case VIEWER:
+				permissionList = []*m.DashboardAcl{
+					newViewerRolePermission(childDashboardID, p),
+				}
+			}
+
+			_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, permissionList)
+			sc.updatePermissions = permissionList
+			ok, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, permissionList)
+
+			if err != nil {
+				sc.reportFailure(tc, nil, err)
+			}
+			if !ok {
+				sc.reportFailure(tc, false, true)
+			}
+			sc.reportSuccess()
+		})
 	}
-	permissionScenario(fmt.Sprintf("and everyone with %s role can %s item", role, permission), sc, p, fn)
 }

+ 256 - 0
pkg/services/guardian/guardian_util_test.go

@@ -0,0 +1,256 @@
+package guardian
+
+import (
+	"bytes"
+	"fmt"
+	"strings"
+	"testing"
+
+	"github.com/grafana/grafana/pkg/bus"
+	m "github.com/grafana/grafana/pkg/models"
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+type scenarioContext struct {
+	t                  *testing.T
+	orgRoleScenario    string
+	permissionScenario string
+	g                  DashboardGuardian
+	givenUser          *m.SignedInUser
+	givenDashboardID   int64
+	givenPermissions   []*m.DashboardAclInfoDTO
+	givenTeams         []*m.Team
+	updatePermissions  []*m.DashboardAcl
+	expectedFlags      permissionFlags
+	callerFile         string
+	callerLine         int
+}
+
+type scenarioFunc func(c *scenarioContext)
+
+func orgRoleScenario(desc string, t *testing.T, role m.RoleType, fn scenarioFunc) {
+	user := &m.SignedInUser{
+		UserId:  userID,
+		OrgId:   orgID,
+		OrgRole: role,
+	}
+	guard := New(dashboardID, orgID, user)
+	sc := &scenarioContext{
+		t:                t,
+		orgRoleScenario:  desc,
+		givenUser:        user,
+		givenDashboardID: dashboardID,
+		g:                guard,
+	}
+
+	Convey(desc, func() {
+		fn(sc)
+	})
+}
+
+func permissionScenario(desc string, dashboardID int64, sc *scenarioContext, permissions []*m.DashboardAclInfoDTO, fn scenarioFunc) {
+	bus.ClearBusHandlers()
+
+	bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
+		if query.OrgId != sc.givenUser.OrgId {
+			sc.reportFailure("Invalid organization id for GetDashboardAclInfoListQuery", sc.givenUser.OrgId, query.OrgId)
+		}
+		if query.DashboardId != sc.givenDashboardID {
+			sc.reportFailure("Invalid dashboard id for GetDashboardAclInfoListQuery", sc.givenDashboardID, query.DashboardId)
+		}
+
+		query.Result = permissions
+		return nil
+	})
+
+	teams := []*m.Team{}
+
+	for _, p := range permissions {
+		if p.TeamId > 0 {
+			teams = append(teams, &m.Team{Id: p.TeamId})
+		}
+	}
+
+	bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
+		if query.OrgId != sc.givenUser.OrgId {
+			sc.reportFailure("Invalid organization id for GetTeamsByUserQuery", sc.givenUser.OrgId, query.OrgId)
+		}
+		if query.UserId != sc.givenUser.UserId {
+			sc.reportFailure("Invalid user id for GetTeamsByUserQuery", sc.givenUser.UserId, query.UserId)
+		}
+
+		query.Result = teams
+		return nil
+	})
+
+	sc.permissionScenario = desc
+	sc.g = New(dashboardID, sc.givenUser.OrgId, sc.givenUser)
+	sc.givenDashboardID = dashboardID
+	sc.givenPermissions = permissions
+	sc.givenTeams = teams
+
+	Convey(desc, func() {
+		fn(sc)
+	})
+}
+
+type permissionType uint8
+
+const (
+	USER permissionType = 1 << iota
+	TEAM
+	EDITOR
+	VIEWER
+)
+
+func (p permissionType) String() string {
+	names := map[uint8]string{
+		uint8(USER):   "user",
+		uint8(TEAM):   "team",
+		uint8(EDITOR): "editor role",
+		uint8(VIEWER): "viewer role",
+	}
+	return names[uint8(p)]
+}
+
+type permissionFlags uint8
+
+const (
+	NO_ACCESS permissionFlags = 1 << iota
+	CAN_ADMIN
+	CAN_EDIT
+	CAN_SAVE
+	CAN_VIEW
+	FULL_ACCESS   = CAN_ADMIN | CAN_EDIT | CAN_SAVE | CAN_VIEW
+	EDITOR_ACCESS = CAN_EDIT | CAN_SAVE | CAN_VIEW
+	VIEWER_ACCESS = CAN_VIEW
+)
+
+func (flag permissionFlags) canAdmin() bool {
+	return flag&CAN_ADMIN != 0
+}
+
+func (flag permissionFlags) canEdit() bool {
+	return flag&CAN_EDIT != 0
+}
+
+func (flag permissionFlags) canSave() bool {
+	return flag&CAN_SAVE != 0
+}
+
+func (flag permissionFlags) canView() bool {
+	return flag&CAN_VIEW != 0
+}
+
+func (flag permissionFlags) noAccess() bool {
+	return flag&(CAN_ADMIN|CAN_EDIT|CAN_SAVE|CAN_VIEW) == 0
+}
+
+func (f permissionFlags) String() string {
+	r := []string{}
+
+	if f.canAdmin() {
+		r = append(r, "admin")
+	}
+
+	if f.canEdit() {
+		r = append(r, "edit")
+	}
+
+	if f.canSave() {
+		r = append(r, "save")
+	}
+
+	if f.canView() {
+		r = append(r, "view")
+	}
+
+	if f.noAccess() {
+		r = append(r, "<no access>")
+	}
+
+	return strings.Join(r[:], ", ")
+}
+
+func (sc *scenarioContext) reportSuccess() {
+	So(true, ShouldBeTrue)
+}
+
+func (sc *scenarioContext) reportFailure(desc string, expected interface{}, actual interface{}) {
+	var buf bytes.Buffer
+	buf.WriteString("\n")
+	buf.WriteString(sc.orgRoleScenario)
+	buf.WriteString(" ")
+	buf.WriteString(sc.permissionScenario)
+	buf.WriteString("\n  ")
+	buf.WriteString(desc)
+	buf.WriteString("\n")
+	buf.WriteString(fmt.Sprintf("Source test: %s:%d\n", sc.callerFile, sc.callerLine))
+	buf.WriteString(fmt.Sprintf("Expected: %v\n", expected))
+	buf.WriteString(fmt.Sprintf("Actual: %v\n", actual))
+	buf.WriteString("Context:")
+	buf.WriteString(fmt.Sprintf("\n  Given user: orgRole=%s, id=%d, orgId=%d", sc.givenUser.OrgRole, sc.givenUser.UserId, sc.givenUser.OrgId))
+	buf.WriteString(fmt.Sprintf("\n  Given dashboard id: %d", sc.givenDashboardID))
+
+	for i, p := range sc.givenPermissions {
+		r := "<nil>"
+		if p.Role != nil {
+			r = string(*p.Role)
+		}
+		buf.WriteString(fmt.Sprintf("\n  Given permission (%d): dashboardId=%d, userId=%d, teamId=%d, role=%v, permission=%s", i, p.DashboardId, p.UserId, p.TeamId, r, p.Permission.String()))
+	}
+
+	for i, t := range sc.givenTeams {
+		buf.WriteString(fmt.Sprintf("\n  Given team (%d): id=%d", i, t.Id))
+	}
+
+	for i, p := range sc.updatePermissions {
+		r := "<nil>"
+		if p.Role != nil {
+			r = string(*p.Role)
+		}
+		buf.WriteString(fmt.Sprintf("\n  Update permission (%d): dashboardId=%d, userId=%d, teamId=%d, role=%v, permission=%s", i, p.DashboardId, p.UserId, p.TeamId, r, p.Permission.String()))
+	}
+
+	sc.t.Fatalf(buf.String())
+}
+
+func newCustomUserPermission(dashboardID int64, userID int64, permission m.PermissionType) *m.DashboardAcl {
+	return &m.DashboardAcl{OrgId: orgID, DashboardId: dashboardID, UserId: userID, Permission: permission}
+}
+
+func newDefaultUserPermission(dashboardID int64, permission m.PermissionType) *m.DashboardAcl {
+	return newCustomUserPermission(dashboardID, userID, permission)
+}
+
+func newCustomTeamPermission(dashboardID int64, teamID int64, permission m.PermissionType) *m.DashboardAcl {
+	return &m.DashboardAcl{OrgId: orgID, DashboardId: dashboardID, TeamId: teamID, Permission: permission}
+}
+
+func newDefaultTeamPermission(dashboardID int64, permission m.PermissionType) *m.DashboardAcl {
+	return newCustomTeamPermission(dashboardID, teamID, permission)
+}
+
+func newAdminRolePermission(dashboardID int64, permission m.PermissionType) *m.DashboardAcl {
+	return &m.DashboardAcl{OrgId: orgID, DashboardId: dashboardID, Role: &adminRole, Permission: permission}
+}
+
+func newEditorRolePermission(dashboardID int64, permission m.PermissionType) *m.DashboardAcl {
+	return &m.DashboardAcl{OrgId: orgID, DashboardId: dashboardID, Role: &editorRole, Permission: permission}
+}
+
+func newViewerRolePermission(dashboardID int64, permission m.PermissionType) *m.DashboardAcl {
+	return &m.DashboardAcl{OrgId: orgID, DashboardId: dashboardID, Role: &viewerRole, Permission: permission}
+}
+
+func toDto(acl *m.DashboardAcl) *m.DashboardAclInfoDTO {
+	return &m.DashboardAclInfoDTO{
+		OrgId:          acl.OrgId,
+		DashboardId:    acl.DashboardId,
+		UserId:         acl.UserId,
+		TeamId:         acl.TeamId,
+		Role:           acl.Role,
+		Permission:     acl.Permission,
+		PermissionName: acl.Permission.String(),
+	}
+}

+ 1 - 2
pkg/services/notifications/mailer.go

@@ -7,7 +7,6 @@ package notifications
 import (
 	"bytes"
 	"crypto/tls"
-	"errors"
 	"fmt"
 	"html/template"
 	"net"
@@ -135,7 +134,7 @@ func buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) {
 		subjectText, hasSubject := subjectData["value"]
 
 		if !hasSubject {
-			return nil, errors.New(fmt.Sprintf("Missing subject in Template %s", cmd.Template))
+			return nil, fmt.Errorf("Missing subject in Template %s", cmd.Template)
 		}
 
 		subjectTmpl, err := template.New("subject").Parse(subjectText.(string))

+ 0 - 3
pkg/services/provisioning/dashboards/types.go

@@ -55,9 +55,6 @@ func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *Das
 	dash.OrgId = cfg.OrgId
 	dash.Dashboard.OrgId = cfg.OrgId
 	dash.Dashboard.FolderId = folderId
-	if !cfg.Editable {
-		dash.Dashboard.Data.Set("editable", cfg.Editable)
-	}
 
 	if dash.Dashboard.Title == "" {
 		return nil, models.ErrDashboardTitleEmpty

+ 1 - 5
pkg/services/provisioning/provisioning.go

@@ -20,11 +20,7 @@ func Init(ctx context.Context, homePath string, cfg *ini.File) error {
 
 	dashboardPath := path.Join(provisioningPath, "dashboards")
 	_, err := dashboards.Provision(ctx, dashboardPath)
-	if err != nil {
-		return err
-	}
-
-	return nil
+	return err
 }
 
 func makeAbsolute(path string, root string) string {

+ 1 - 6
pkg/services/sqlstore/alert_notification.go

@@ -23,12 +23,7 @@ func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error {
 	return inTransaction(func(sess *DBSession) error {
 		sql := "DELETE FROM alert_notification WHERE alert_notification.org_id = ? AND alert_notification.id = ?"
 		_, err := sess.Exec(sql, cmd.OrgId, cmd.Id)
-
-		if err != nil {
-			return err
-		}
-
-		return nil
+		return err
 	})
 }
 

+ 2 - 5
pkg/services/sqlstore/annotation.go

@@ -102,11 +102,8 @@ func (r *SqlAnnotationRepo) Update(item *annotations.Item) error {
 
 		existing.Tags = item.Tags
 
-		if _, err := sess.Table("annotation").Id(existing.Id).Cols("epoch", "text", "region_id", "tags").Update(existing); err != nil {
-			return err
-		}
-
-		return nil
+		_, err = sess.Table("annotation").Id(existing.Id).Cols("epoch", "text", "region_id", "tags").Update(existing)
+		return err
 	})
 }
 

+ 2 - 2
pkg/services/sqlstore/apikey.go

@@ -55,7 +55,7 @@ func GetApiKeyById(query *m.GetApiKeyByIdQuery) error {
 
 	if err != nil {
 		return err
-	} else if has == false {
+	} else if !has {
 		return m.ErrInvalidApiKey
 	}
 
@@ -69,7 +69,7 @@ func GetApiKeyByName(query *m.GetApiKeyByNameQuery) error {
 
 	if err != nil {
 		return err
-	} else if has == false {
+	} else if !has {
 		return m.ErrInvalidApiKey
 	}
 

+ 21 - 18
pkg/services/sqlstore/dashboard.go

@@ -63,7 +63,7 @@ func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error {
 		}
 
 		// do not allow plugin dashboard updates without overwrite flag
-		if existing.PluginId != "" && cmd.Overwrite == false {
+		if existing.PluginId != "" && !cmd.Overwrite {
 			return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
 		}
 	}
@@ -172,7 +172,7 @@ func GetDashboard(query *m.GetDashboardQuery) error {
 
 	if err != nil {
 		return err
-	} else if has == false {
+	} else if !has {
 		return m.ErrDashboardNotFound
 	}
 
@@ -308,7 +308,7 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
 		has, err := sess.Get(&dashboard)
 		if err != nil {
 			return err
-		} else if has == false {
+		} else if !has {
 			return m.ErrDashboardNotFound
 		}
 
@@ -347,12 +347,7 @@ func GetDashboards(query *m.GetDashboardsQuery) error {
 
 	err := x.In("id", query.DashboardIds).Find(&dashboards)
 	query.Result = dashboards
-
-	if err != nil {
-		return err
-	}
-
-	return nil
+	return err
 }
 
 // GetDashboardPermissionsForUser returns the maximum permission the specified user has for a dashboard(s)
@@ -431,12 +426,7 @@ func GetDashboardsByPluginId(query *m.GetDashboardsByPluginIdQuery) error {
 
 	err := x.Where(whereExpr, query.OrgId, query.PluginId).Find(&dashboards)
 	query.Result = dashboards
-
-	if err != nil {
-		return err
-	}
-
-	return nil
+	return err
 }
 
 type DashboardSlugDTO struct {
@@ -451,7 +441,7 @@ func GetDashboardSlugById(query *m.GetDashboardSlugByIdQuery) error {
 
 	if err != nil {
 		return err
-	} else if exists == false {
+	} else if !exists {
 		return m.ErrDashboardNotFound
 	}
 
@@ -479,7 +469,7 @@ func GetDashboardUIDById(query *m.GetDashboardRefByIdQuery) error {
 
 	if err != nil {
 		return err
-	} else if exists == false {
+	} else if !exists {
 		return m.ErrDashboardNotFound
 	}
 
@@ -544,6 +534,10 @@ func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *m.ValidateDash
 		dash.SetId(existingByUid.Id)
 		dash.SetUid(existingByUid.Uid)
 		existing = existingByUid
+
+		if !dash.IsFolder {
+			cmd.Result.IsParentFolderChanged = true
+		}
 	}
 
 	if (existing.IsFolder && !dash.IsFolder) ||
@@ -551,6 +545,10 @@ func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *m.ValidateDash
 		return m.ErrDashboardTypeMismatch
 	}
 
+	if !dash.IsFolder && dash.FolderId != existing.FolderId {
+		cmd.Result.IsParentFolderChanged = true
+	}
+
 	// check for is someone else has written in between
 	if dash.Version != existing.Version {
 		if cmd.Overwrite {
@@ -561,7 +559,7 @@ func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *m.ValidateDash
 	}
 
 	// do not allow plugin dashboard updates without overwrite flag
-	if existing.PluginId != "" && cmd.Overwrite == false {
+	if existing.PluginId != "" && !cmd.Overwrite {
 		return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
 	}
 
@@ -586,6 +584,10 @@ func getExistingDashboardByTitleAndFolder(sess *DBSession, cmd *m.ValidateDashbo
 			return m.ErrDashboardFolderWithSameNameAsDashboard
 		}
 
+		if !dash.IsFolder && (dash.FolderId != existing.FolderId || dash.Id == 0) {
+			cmd.Result.IsParentFolderChanged = true
+		}
+
 		if cmd.Overwrite {
 			dash.SetId(existing.Id)
 			dash.SetUid(existing.Uid)
@@ -599,6 +601,7 @@ func getExistingDashboardByTitleAndFolder(sess *DBSession, cmd *m.ValidateDashbo
 }
 
 func ValidateDashboardBeforeSave(cmd *m.ValidateDashboardBeforeSaveCommand) (err error) {
+	cmd.Result = &m.ValidateDashboardBeforeSaveResult{}
 	return inTransaction(func(sess *DBSession) error {
 		if err = getExistingDashboardByIdOrUidForUpdate(sess, cmd); err != nil {
 			return err

+ 3 - 4
pkg/services/sqlstore/dashboard_acl.go

@@ -35,10 +35,8 @@ func UpdateDashboardAcl(cmd *m.UpdateDashboardAclCommand) error {
 
 		// Update dashboard HasAcl flag
 		dashboard := m.Dashboard{HasAcl: true}
-		if _, err := sess.Cols("has_acl").Where("id=?", cmd.DashboardId).Update(&dashboard); err != nil {
-			return err
-		}
-		return nil
+		_, err = sess.Cols("has_acl").Where("id=?", cmd.DashboardId).Update(&dashboard)
+		return err
 	})
 }
 
@@ -92,6 +90,7 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
 				u.login AS user_login,
 				u.email AS user_email,
 				ug.name AS team,
+				ug.email AS team_email,
 				d.title,
 				d.slug,
 				d.uid,

+ 14 - 0
pkg/services/sqlstore/dashboard_provisioning.go

@@ -8,6 +8,7 @@ import (
 func init() {
 	bus.AddHandler("sql", GetProvisionedDashboardDataQuery)
 	bus.AddHandler("sql", SaveProvisionedDashboard)
+	bus.AddHandler("sql", GetProvisionedDataByDashboardId)
 }
 
 type DashboardExtras struct {
@@ -17,6 +18,19 @@ type DashboardExtras struct {
 	Value       string
 }
 
+func GetProvisionedDataByDashboardId(cmd *models.IsDashboardProvisionedQuery) error {
+	result := &models.DashboardProvisioning{}
+
+	exist, err := x.Where("dashboard_id = ?", cmd.DashboardId).Get(result)
+	if err != nil {
+		return err
+	}
+
+	cmd.Result = exist
+
+	return nil
+}
+
 func SaveProvisionedDashboard(cmd *models.SaveProvisionedDashboardCommand) error {
 	return inTransaction(func(sess *DBSession) error {
 		err := saveDashboard(sess, cmd.DashboardCmd)

+ 17 - 0
pkg/services/sqlstore/dashboard_provisioning_test.go

@@ -50,6 +50,23 @@ func TestDashboardProvisioningTest(t *testing.T) {
 				So(query.Result[0].DashboardId, ShouldEqual, dashId)
 				So(query.Result[0].Updated, ShouldEqual, now.Unix())
 			})
+
+			Convey("Can query for one provisioned dashboard", func() {
+				query := &models.IsDashboardProvisionedQuery{DashboardId: cmd.Result.Id}
+
+				err := GetProvisionedDataByDashboardId(query)
+				So(err, ShouldBeNil)
+
+				So(query.Result, ShouldBeTrue)
+			})
+
+			Convey("Can query for none provisioned dashboard", func() {
+				query := &models.IsDashboardProvisionedQuery{DashboardId: 3000}
+
+				err := GetProvisionedDataByDashboardId(query)
+				So(err, ShouldBeNil)
+				So(query.Result, ShouldBeFalse)
+			})
 		})
 	})
 }

+ 165 - 35
pkg/services/sqlstore/dashboard_service_integration_test.go

@@ -19,7 +19,6 @@ func TestIntegratedDashboardService(t *testing.T) {
 		var testOrgId int64 = 1
 
 		Convey("Given saved folders and dashboards in organization A", func() {
-
 			bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
 				return nil
 			})
@@ -28,6 +27,11 @@ func TestIntegratedDashboardService(t *testing.T) {
 				return nil
 			})
 
+			bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
+				cmd.Result = false
+				return nil
+			})
+
 			savedFolder := saveTestFolder("Saved folder", testOrgId)
 			savedDashInFolder := saveTestDashboard("Saved dash in folder", testOrgId, savedFolder.Id)
 			saveTestDashboard("Other saved dash in folder", testOrgId, savedFolder.Id)
@@ -74,7 +78,7 @@ func TestIntegratedDashboardService(t *testing.T) {
 			Convey("Given organization B", func() {
 				var otherOrgId int64 = 2
 
-				Convey("When saving a dashboard with id that are saved in organization A", func() {
+				Convey("When creating a dashboard with same id as dashboard in organization A", func() {
 					cmd := models.SaveDashboardCommand{
 						OrgId: otherOrgId,
 						Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -93,7 +97,7 @@ func TestIntegratedDashboardService(t *testing.T) {
 				})
 
 				permissionScenario("Given user has permission to save", true, func(sc *dashboardPermissionScenarioContext) {
-					Convey("When saving a dashboard with uid that are saved in organization A", func() {
+					Convey("When creating a dashboard with same uid as dashboard in organization A", func() {
 						var otherOrgId int64 = 2
 						cmd := models.SaveDashboardCommand{
 							OrgId: otherOrgId,
@@ -106,7 +110,7 @@ func TestIntegratedDashboardService(t *testing.T) {
 
 						res := callSaveWithResult(cmd)
 
-						Convey("It should create dashboard in other organization", func() {
+						Convey("It should create a new dashboard in organization B", func() {
 							So(res, ShouldNotBeNil)
 
 							query := models.GetDashboardQuery{OrgId: otherOrgId, Uid: savedDashInFolder.Uid}
@@ -126,7 +130,7 @@ func TestIntegratedDashboardService(t *testing.T) {
 
 			permissionScenario("Given user has no permission to save", false, func(sc *dashboardPermissionScenarioContext) {
 
-				Convey("When trying to create a new dashboard in the General folder", func() {
+				Convey("When creating a new dashboard in the General folder", func() {
 					cmd := models.SaveDashboardCommand{
 						OrgId: testOrgId,
 						Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -138,7 +142,7 @@ func TestIntegratedDashboardService(t *testing.T) {
 
 					err := callSaveWithError(cmd)
 
-					Convey("It should call dashboard guardian with correct arguments and result in access denied error", func() {
+					Convey("It should create dashboard guardian for General Folder with correct arguments and result in access denied error", func() {
 						So(err, ShouldNotBeNil)
 						So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
 
@@ -148,7 +152,7 @@ func TestIntegratedDashboardService(t *testing.T) {
 					})
 				})
 
-				Convey("When trying to create a new dashboard in other folder", func() {
+				Convey("When creating a new dashboard in other folder", func() {
 					cmd := models.SaveDashboardCommand{
 						OrgId: testOrgId,
 						Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -161,7 +165,7 @@ func TestIntegratedDashboardService(t *testing.T) {
 
 					err := callSaveWithError(cmd)
 
-					Convey("It should call dashboard guardian with correct arguments and rsult in access denied error", func() {
+					Convey("It should create dashboard guardian for other folder with correct arguments and rsult in access denied error", func() {
 						So(err, ShouldNotBeNil)
 						So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
 
@@ -171,7 +175,54 @@ func TestIntegratedDashboardService(t *testing.T) {
 					})
 				})
 
-				Convey("When trying to update a dashboard by existing id in the General folder", func() {
+				Convey("When creating a new dashboard by existing title in folder", func() {
+					cmd := models.SaveDashboardCommand{
+						OrgId: testOrgId,
+						Dashboard: simplejson.NewFromAny(map[string]interface{}{
+							"title": savedDashInFolder.Title,
+						}),
+						FolderId:  savedFolder.Id,
+						UserId:    10000,
+						Overwrite: true,
+					}
+
+					err := callSaveWithError(cmd)
+
+					Convey("It should create dashboard guardian for folder with correct arguments and result in access denied error", func() {
+						So(err, ShouldNotBeNil)
+						So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
+
+						So(sc.dashboardGuardianMock.DashId, ShouldEqual, savedFolder.Id)
+						So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
+						So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
+					})
+				})
+
+				Convey("When creating a new dashboard by existing uid in folder", func() {
+					cmd := models.SaveDashboardCommand{
+						OrgId: testOrgId,
+						Dashboard: simplejson.NewFromAny(map[string]interface{}{
+							"uid":   savedDashInFolder.Uid,
+							"title": "New dash",
+						}),
+						FolderId:  savedFolder.Id,
+						UserId:    10000,
+						Overwrite: true,
+					}
+
+					err := callSaveWithError(cmd)
+
+					Convey("It should create dashboard guardian for folder with correct arguments and result in access denied error", func() {
+						So(err, ShouldNotBeNil)
+						So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
+
+						So(sc.dashboardGuardianMock.DashId, ShouldEqual, savedFolder.Id)
+						So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
+						So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
+					})
+				})
+
+				Convey("When updating a dashboard by existing id in the General folder", func() {
 					cmd := models.SaveDashboardCommand{
 						OrgId: testOrgId,
 						Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -185,7 +236,7 @@ func TestIntegratedDashboardService(t *testing.T) {
 
 					err := callSaveWithError(cmd)
 
-					Convey("It should call dashboard guardian with correct arguments and result in access denied error", func() {
+					Convey("It should create dashboard guardian for dashboard with correct arguments and result in access denied error", func() {
 						So(err, ShouldNotBeNil)
 						So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
 
@@ -195,7 +246,7 @@ func TestIntegratedDashboardService(t *testing.T) {
 					})
 				})
 
-				Convey("When trying to update a dashboard by existing id in other folder", func() {
+				Convey("When updating a dashboard by existing id in other folder", func() {
 					cmd := models.SaveDashboardCommand{
 						OrgId: testOrgId,
 						Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -209,7 +260,7 @@ func TestIntegratedDashboardService(t *testing.T) {
 
 					err := callSaveWithError(cmd)
 
-					Convey("It should call dashboard guardian with correct arguments and result in access denied error", func() {
+					Convey("It should create dashboard guardian for dashboard with correct arguments and result in access denied error", func() {
 						So(err, ShouldNotBeNil)
 						So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
 
@@ -218,6 +269,102 @@ func TestIntegratedDashboardService(t *testing.T) {
 						So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
 					})
 				})
+
+				Convey("When moving a dashboard by existing id to other folder from General folder", func() {
+					cmd := models.SaveDashboardCommand{
+						OrgId: testOrgId,
+						Dashboard: simplejson.NewFromAny(map[string]interface{}{
+							"id":    savedDashInGeneralFolder.Id,
+							"title": "Dash",
+						}),
+						FolderId:  otherSavedFolder.Id,
+						UserId:    10000,
+						Overwrite: true,
+					}
+
+					err := callSaveWithError(cmd)
+
+					Convey("It should create dashboard guardian for other folder with correct arguments and result in access denied error", func() {
+						So(err, ShouldNotBeNil)
+						So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
+
+						So(sc.dashboardGuardianMock.DashId, ShouldEqual, otherSavedFolder.Id)
+						So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
+						So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
+					})
+				})
+
+				Convey("When moving a dashboard by existing id to the General folder from other folder", func() {
+					cmd := models.SaveDashboardCommand{
+						OrgId: testOrgId,
+						Dashboard: simplejson.NewFromAny(map[string]interface{}{
+							"id":    savedDashInFolder.Id,
+							"title": "Dash",
+						}),
+						FolderId:  0,
+						UserId:    10000,
+						Overwrite: true,
+					}
+
+					err := callSaveWithError(cmd)
+
+					Convey("It should create dashboard guardian for General folder with correct arguments and result in access denied error", func() {
+						So(err, ShouldNotBeNil)
+						So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
+
+						So(sc.dashboardGuardianMock.DashId, ShouldEqual, 0)
+						So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
+						So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
+					})
+				})
+
+				Convey("When moving a dashboard by existing uid to other folder from General folder", func() {
+					cmd := models.SaveDashboardCommand{
+						OrgId: testOrgId,
+						Dashboard: simplejson.NewFromAny(map[string]interface{}{
+							"uid":   savedDashInGeneralFolder.Uid,
+							"title": "Dash",
+						}),
+						FolderId:  otherSavedFolder.Id,
+						UserId:    10000,
+						Overwrite: true,
+					}
+
+					err := callSaveWithError(cmd)
+
+					Convey("It should create dashboard guardian for other folder with correct arguments and result in access denied error", func() {
+						So(err, ShouldNotBeNil)
+						So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
+
+						So(sc.dashboardGuardianMock.DashId, ShouldEqual, otherSavedFolder.Id)
+						So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
+						So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
+					})
+				})
+
+				Convey("When moving a dashboard by existing uid to the General folder from other folder", func() {
+					cmd := models.SaveDashboardCommand{
+						OrgId: testOrgId,
+						Dashboard: simplejson.NewFromAny(map[string]interface{}{
+							"uid":   savedDashInFolder.Uid,
+							"title": "Dash",
+						}),
+						FolderId:  0,
+						UserId:    10000,
+						Overwrite: true,
+					}
+
+					err := callSaveWithError(cmd)
+
+					Convey("It should create dashboard guardian for General folder with correct arguments and result in access denied error", func() {
+						So(err, ShouldNotBeNil)
+						So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
+
+						So(sc.dashboardGuardianMock.DashId, ShouldEqual, 0)
+						So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
+						So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
+					})
+				})
 			})
 
 			// Given user has permission to save
@@ -668,7 +815,7 @@ func TestIntegratedDashboardService(t *testing.T) {
 						})
 					})
 
-					Convey("When trying to update existing folder to a dashboard using id", func() {
+					Convey("When updating existing folder to a dashboard using id", func() {
 						cmd := models.SaveDashboardCommand{
 							OrgId: 1,
 							Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -687,7 +834,7 @@ func TestIntegratedDashboardService(t *testing.T) {
 						})
 					})
 
-					Convey("When trying to update existing dashboard to a folder using id", func() {
+					Convey("When updating existing dashboard to a folder using id", func() {
 						cmd := models.SaveDashboardCommand{
 							OrgId: 1,
 							Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -706,7 +853,7 @@ func TestIntegratedDashboardService(t *testing.T) {
 						})
 					})
 
-					Convey("When trying to update existing folder to a dashboard using uid", func() {
+					Convey("When updating existing folder to a dashboard using uid", func() {
 						cmd := models.SaveDashboardCommand{
 							OrgId: 1,
 							Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -725,7 +872,7 @@ func TestIntegratedDashboardService(t *testing.T) {
 						})
 					})
 
-					Convey("When trying to update existing dashboard to a folder using uid", func() {
+					Convey("When updating existing dashboard to a folder using uid", func() {
 						cmd := models.SaveDashboardCommand{
 							OrgId: 1,
 							Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -744,7 +891,7 @@ func TestIntegratedDashboardService(t *testing.T) {
 						})
 					})
 
-					Convey("When trying to update existing folder to a dashboard using title", func() {
+					Convey("When updating existing folder to a dashboard using title", func() {
 						cmd := models.SaveDashboardCommand{
 							OrgId: 1,
 							Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -762,7 +909,7 @@ func TestIntegratedDashboardService(t *testing.T) {
 						})
 					})
 
-					Convey("When trying to update existing dashboard to a folder using title", func() {
+					Convey("When updating existing dashboard to a folder using title", func() {
 						cmd := models.SaveDashboardCommand{
 							OrgId: 1,
 							Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -850,23 +997,6 @@ func callSaveWithError(cmd models.SaveDashboardCommand) error {
 	return err
 }
 
-func dashboardServiceScenario(desc string, mock *guardian.FakeDashboardGuardian, fn scenarioFunc) {
-	Convey(desc, func() {
-		origNewDashboardGuardian := guardian.New
-		guardian.MockDashboardGuardian(mock)
-
-		sc := &scenarioContext{
-			dashboardGuardianMock: mock,
-		}
-
-		defer func() {
-			guardian.New = origNewDashboardGuardian
-		}()
-
-		fn(sc)
-	})
-}
-
 func saveTestDashboard(title string, orgId int64, folderId int64) *models.Dashboard {
 	cmd := models.SaveDashboardCommand{
 		OrgId:    orgId,

+ 1 - 1
pkg/services/sqlstore/dashboard_snapshot.go

@@ -80,7 +80,7 @@ func GetDashboardSnapshot(query *m.GetDashboardSnapshotQuery) error {
 
 	if err != nil {
 		return err
-	} else if has == false {
+	} else if !has {
 		return m.ErrDashboardSnapshotNotFound
 	}
 

+ 2 - 4
pkg/services/sqlstore/migrator/dialect.go

@@ -84,8 +84,7 @@ func (db *BaseDialect) DateTimeFunc(value string) string {
 }
 
 func (b *BaseDialect) CreateTableSql(table *Table) string {
-	var sql string
-	sql = "CREATE TABLE IF NOT EXISTS "
+	sql := "CREATE TABLE IF NOT EXISTS "
 	sql += b.dialect.Quote(table.Name) + " (\n"
 
 	pkList := table.PrimaryKeys
@@ -162,8 +161,7 @@ func (db *BaseDialect) RenameTable(oldName string, newName string) string {
 
 func (db *BaseDialect) DropIndexSql(tableName string, index *Index) string {
 	quote := db.dialect.Quote
-	var name string
-	name = index.XName(tableName)
+	name := index.XName(tableName)
 	return fmt.Sprintf("DROP INDEX %v ON %s", quote(name), quote(tableName))
 }
 

+ 1 - 2
pkg/services/sqlstore/migrator/migrations.go

@@ -1,7 +1,6 @@
 package migrator
 
 import (
-	"fmt"
 	"strings"
 )
 
@@ -113,7 +112,7 @@ func NewDropIndexMigration(table Table, index *Index) *DropIndexMigration {
 
 func (m *DropIndexMigration) Sql(dialect Dialect) string {
 	if m.index.Name == "" {
-		m.index.Name = fmt.Sprintf("%s", strings.Join(m.index.Cols, "_"))
+		m.index.Name = strings.Join(m.index.Cols, "_")
 	}
 	return dialect.DropIndexSql(m.tableName, m.index)
 }

+ 1 - 1
pkg/services/sqlstore/migrator/types.go

@@ -46,7 +46,7 @@ type Index struct {
 
 func (index *Index) XName(tableName string) string {
 	if index.Name == "" {
-		index.Name = fmt.Sprintf("%s", strings.Join(index.Cols, "_"))
+		index.Name = strings.Join(index.Cols, "_")
 	}
 
 	if !strings.HasPrefix(index.Name, "UQE_") &&

+ 1 - 1
pkg/services/sqlstore/plugin_setting.go

@@ -36,7 +36,7 @@ func GetPluginSettingById(query *m.GetPluginSettingByIdQuery) error {
 	has, err := x.Get(&pluginSetting)
 	if err != nil {
 		return err
-	} else if has == false {
+	} else if !has {
 		return m.ErrPluginSettingNotFound
 	}
 	query.Result = &pluginSetting

+ 4 - 4
pkg/services/sqlstore/quota.go

@@ -31,7 +31,7 @@ func GetOrgQuotaByTarget(query *m.GetOrgQuotaByTargetQuery) error {
 	has, err := x.Get(&quota)
 	if err != nil {
 		return err
-	} else if has == false {
+	} else if !has {
 		quota.Limit = query.Default
 	}
 
@@ -108,7 +108,7 @@ func UpdateOrgQuota(cmd *m.UpdateOrgQuotaCmd) error {
 			return err
 		}
 		quota.Limit = cmd.Limit
-		if has == false {
+		if !has {
 			quota.Created = time.Now()
 			//No quota in the DB for this target, so create a new one.
 			if _, err := sess.Insert(&quota); err != nil {
@@ -133,7 +133,7 @@ func GetUserQuotaByTarget(query *m.GetUserQuotaByTargetQuery) error {
 	has, err := x.Get(&quota)
 	if err != nil {
 		return err
-	} else if has == false {
+	} else if !has {
 		quota.Limit = query.Default
 	}
 
@@ -210,7 +210,7 @@ func UpdateUserQuota(cmd *m.UpdateUserQuotaCmd) error {
 			return err
 		}
 		quota.Limit = cmd.Limit
-		if has == false {
+		if !has {
 			quota.Created = time.Now()
 			//No quota in the DB for this target, so create a new one.
 			if _, err := sess.Insert(&quota); err != nil {

+ 1 - 4
pkg/services/sqlstore/stats.go

@@ -19,10 +19,6 @@ func GetDataSourceStats(query *m.GetDataSourceStatsQuery) error {
 	var rawSql = `SELECT COUNT(*) as count, type FROM data_source GROUP BY type`
 	query.Result = make([]*m.DataSourceStats, 0)
 	err := x.SQL(rawSql).Find(&query.Result)
-	if err != nil {
-		return err
-	}
-
 	return err
 }
 
@@ -68,6 +64,7 @@ func GetSystemStats(query *m.GetSystemStatsQuery) error {
 	}
 
 	query.Result = &stats
+
 	return err
 }
 

+ 1 - 5
pkg/services/sqlstore/team.go

@@ -210,11 +210,7 @@ func GetTeamsByUser(query *m.GetTeamsByUserQuery) error {
 	sess.Where("team.org_id=? and team_member.user_id=?", query.OrgId, query.UserId)
 
 	err := sess.Find(&query.Result)
-	if err != nil {
-		return err
-	}
-
-	return nil
+	return err
 }
 
 // AddTeamMember adds a user to a team

+ 1 - 1
pkg/services/sqlstore/temp_user.go

@@ -126,7 +126,7 @@ func GetTempUserByCode(query *m.GetTempUserByCodeQuery) error {
 
 	if err != nil {
 		return err
-	} else if has == false {
+	} else if !has {
 		return m.ErrTempUserNotFound
 	}
 

+ 11 - 20
pkg/services/sqlstore/user.go

@@ -154,7 +154,7 @@ func GetUserById(query *m.GetUserByIdQuery) error {
 
 	if err != nil {
 		return err
-	} else if has == false {
+	} else if !has {
 		return m.ErrUserNotFound
 	}
 
@@ -179,7 +179,7 @@ func GetUserByLogin(query *m.GetUserByLoginQuery) error {
 		return err
 	}
 
-	if has == false && strings.Contains(query.LoginOrEmail, "@") {
+	if !has && strings.Contains(query.LoginOrEmail, "@") {
 		// If the user wasn't found, and it contains an "@" fallback to finding the
 		// user by email.
 		user = &m.User{Email: query.LoginOrEmail}
@@ -188,7 +188,7 @@ func GetUserByLogin(query *m.GetUserByLoginQuery) error {
 
 	if err != nil {
 		return err
-	} else if has == false {
+	} else if !has {
 		return m.ErrUserNotFound
 	}
 
@@ -209,7 +209,7 @@ func GetUserByEmail(query *m.GetUserByEmailQuery) error {
 
 	if err != nil {
 		return err
-	} else if has == false {
+	} else if !has {
 		return m.ErrUserNotFound
 	}
 
@@ -253,11 +253,8 @@ func ChangeUserPassword(cmd *m.ChangeUserPasswordCommand) error {
 			Updated:  time.Now(),
 		}
 
-		if _, err := sess.Id(cmd.UserId).Update(&user); err != nil {
-			return err
-		}
-
-		return nil
+		_, err := sess.Id(cmd.UserId).Update(&user)
+		return err
 	})
 }
 
@@ -271,11 +268,8 @@ func UpdateUserLastSeenAt(cmd *m.UpdateUserLastSeenAtCommand) error {
 			LastSeenAt: time.Now(),
 		}
 
-		if _, err := sess.Id(cmd.UserId).Update(&user); err != nil {
-			return err
-		}
-
-		return nil
+		_, err := sess.Id(cmd.UserId).Update(&user)
+		return err
 	})
 }
 
@@ -310,7 +304,7 @@ func GetUserProfile(query *m.GetUserProfileQuery) error {
 
 	if err != nil {
 		return err
-	} else if has == false {
+	} else if !has {
 		return m.ErrUserNotFound
 	}
 
@@ -479,10 +473,7 @@ func SetUserHelpFlag(cmd *m.SetUserHelpFlagCommand) error {
 			Updated:    time.Now(),
 		}
 
-		if _, err := sess.Id(cmd.UserId).Cols("help_flags1").Update(&user); err != nil {
-			return err
-		}
-
-		return nil
+		_, err := sess.Id(cmd.UserId).Cols("help_flags1").Update(&user)
+		return err
 	})
 }

+ 1 - 1
pkg/social/generic_oauth.go

@@ -182,7 +182,7 @@ func (s *SocialGenericOAuth) UserInfo(client *http.Client, token *oauth2.Token)
 	var data UserInfoJson
 	var err error
 
-	if s.extractToken(&data, token) != true {
+	if !s.extractToken(&data, token) {
 		response, err := HttpGet(client, s.apiUrl)
 		if err != nil {
 			return nil, fmt.Errorf("Error getting user info: %s", err)

+ 2 - 2
pkg/tsdb/cloudwatch/annotation_query.go

@@ -72,7 +72,7 @@ func (e *CloudWatchExecutor) executeAnnotationQuery(ctx context.Context, queryCo
 				MetricName: aws.String(metricName),
 				Dimensions: qd,
 				Statistic:  aws.String(s),
-				Period:     aws.Int64(int64(period)),
+				Period:     aws.Int64(period),
 			}
 			resp, err := svc.DescribeAlarmsForMetric(params)
 			if err != nil {
@@ -88,7 +88,7 @@ func (e *CloudWatchExecutor) executeAnnotationQuery(ctx context.Context, queryCo
 				MetricName:        aws.String(metricName),
 				Dimensions:        qd,
 				ExtendedStatistic: aws.String(s),
-				Period:            aws.Int64(int64(period)),
+				Period:            aws.Int64(period),
 			}
 			resp, err := svc.DescribeAlarmsForMetric(params)
 			if err != nil {

+ 0 - 3
pkg/tsdb/cloudwatch/cloudwatch.go

@@ -71,15 +71,12 @@ func (e *CloudWatchExecutor) Query(ctx context.Context, dsInfo *models.DataSourc
 	switch queryType {
 	case "metricFindQuery":
 		result, err = e.executeMetricFindQuery(ctx, queryContext)
-		break
 	case "annotationQuery":
 		result, err = e.executeAnnotationQuery(ctx, queryContext)
-		break
 	case "timeSeriesQuery":
 		fallthrough
 	default:
 		result, err = e.executeTimeSeriesQuery(ctx, queryContext)
-		break
 	}
 
 	return result, err

+ 4 - 15
pkg/tsdb/cloudwatch/metric_find_query.go

@@ -175,25 +175,18 @@ func (e *CloudWatchExecutor) executeMetricFindQuery(ctx context.Context, queryCo
 	switch subType {
 	case "regions":
 		data, err = e.handleGetRegions(ctx, parameters, queryContext)
-		break
 	case "namespaces":
 		data, err = e.handleGetNamespaces(ctx, parameters, queryContext)
-		break
 	case "metrics":
 		data, err = e.handleGetMetrics(ctx, parameters, queryContext)
-		break
 	case "dimension_keys":
 		data, err = e.handleGetDimensions(ctx, parameters, queryContext)
-		break
 	case "dimension_values":
 		data, err = e.handleGetDimensionValues(ctx, parameters, queryContext)
-		break
 	case "ebs_volume_ids":
 		data, err = e.handleGetEbsVolumeIds(ctx, parameters, queryContext)
-		break
 	case "ec2_instance_attribute":
 		data, err = e.handleGetEc2InstanceAttribute(ctx, parameters, queryContext)
-		break
 	}
 
 	transformToTable(data, queryResult)
@@ -261,7 +254,7 @@ func (e *CloudWatchExecutor) handleGetNamespaces(ctx context.Context, parameters
 		keys = append(keys, strings.Split(customNamespaces, ",")...)
 	}
 
-	sort.Sort(sort.StringSlice(keys))
+	sort.Strings(keys)
 
 	result := make([]suggestData, 0)
 	for _, key := range keys {
@@ -290,7 +283,7 @@ func (e *CloudWatchExecutor) handleGetMetrics(ctx context.Context, parameters *s
 			return nil, errors.New("Unable to call AWS API")
 		}
 	}
-	sort.Sort(sort.StringSlice(namespaceMetrics))
+	sort.Strings(namespaceMetrics)
 
 	result := make([]suggestData, 0)
 	for _, name := range namespaceMetrics {
@@ -319,7 +312,7 @@ func (e *CloudWatchExecutor) handleGetDimensions(ctx context.Context, parameters
 			return nil, errors.New("Unable to call AWS API")
 		}
 	}
-	sort.Sort(sort.StringSlice(dimensionValues))
+	sort.Strings(dimensionValues)
 
 	result := make([]suggestData, 0)
 	for _, name := range dimensionValues {
@@ -573,11 +566,7 @@ func getAllMetrics(cwData *DatasourceInfo) (cloudwatch.ListMetricsOutput, error)
 			}
 			return !lastPage
 		})
-	if err != nil {
-		return resp, err
-	}
-
-	return resp, nil
+	return resp, err
 }
 
 var metricsCacheLock sync.Mutex

+ 1 - 4
pkg/tsdb/cloudwatch/metric_find_query_test.go

@@ -181,10 +181,7 @@ func TestCloudWatchMetrics(t *testing.T) {
 }
 
 func TestParseMultiSelectValue(t *testing.T) {
-
-	var values []string
-
-	values = parseMultiSelectValue(" i-someInstance ")
+	values := parseMultiSelectValue(" i-someInstance ")
 	assert.Equal(t, []string{"i-someInstance"}, values)
 
 	values = parseMultiSelectValue("{i-05}")

+ 5 - 5
pkg/tsdb/mssql/mssql.go

@@ -145,7 +145,7 @@ func (e MssqlQueryEndpoint) getTypedRowData(types []*sql.ColumnType, rows *core.
 	// convert types not handled by denisenkom/go-mssqldb
 	// unhandled types are returned as []byte
 	for i := 0; i < len(types); i++ {
-		if value, ok := values[i].([]byte); ok == true {
+		if value, ok := values[i].([]byte); ok {
 			switch types[i].DatabaseTypeName() {
 			case "MONEY", "SMALLMONEY", "DECIMAL":
 				if v, err := strconv.ParseFloat(string(value), 64); err == nil {
@@ -209,7 +209,7 @@ func (e MssqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
 	fillValue := null.Float{}
 	if fillMissing {
 		fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000
-		if query.Model.Get("fillNull").MustBool(false) == false {
+		if !query.Model.Get("fillNull").MustBool(false) {
 			fillValue.Float64 = query.Model.Get("fillValue").MustFloat64()
 			fillValue.Valid = true
 		}
@@ -244,7 +244,7 @@ func (e MssqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
 		}
 
 		if metricIndex >= 0 {
-			if columnValue, ok := values[metricIndex].(string); ok == true {
+			if columnValue, ok := values[metricIndex].(string); ok {
 				metric = columnValue
 			} else {
 				return fmt.Errorf("Column metric must be of type CHAR, VARCHAR, NCHAR or NVARCHAR. metric column name: %s type: %s but datatype is %T", columnNames[metricIndex], columnTypes[metricIndex].DatabaseTypeName(), values[metricIndex])
@@ -271,7 +271,7 @@ func (e MssqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
 			}
 
 			series, exist := pointsBySeries[metric]
-			if exist == false {
+			if !exist {
 				series = &tsdb.TimeSeries{Name: metric}
 				pointsBySeries[metric] = series
 				seriesByQueryOrder.PushBack(metric)
@@ -279,7 +279,7 @@ func (e MssqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
 
 			if fillMissing {
 				var intervalStart float64
-				if exist == false {
+				if !exist {
 					intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6)
 				} else {
 					intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval

+ 4 - 4
pkg/tsdb/mysql/mysql.go

@@ -218,7 +218,7 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
 	fillValue := null.Float{}
 	if fillMissing {
 		fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000
-		if query.Model.Get("fillNull").MustBool(false) == false {
+		if !query.Model.Get("fillNull").MustBool(false) {
 			fillValue.Float64 = query.Model.Get("fillValue").MustFloat64()
 			fillValue.Valid = true
 		}
@@ -253,7 +253,7 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
 		}
 
 		if metricIndex >= 0 {
-			if columnValue, ok := values[metricIndex].(string); ok == true {
+			if columnValue, ok := values[metricIndex].(string); ok {
 				metric = columnValue
 			} else {
 				return fmt.Errorf("Column metric must be of type char,varchar or text, got: %T %v", values[metricIndex], values[metricIndex])
@@ -280,7 +280,7 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
 			}
 
 			series, exist := pointsBySeries[metric]
-			if exist == false {
+			if !exist {
 				series = &tsdb.TimeSeries{Name: metric}
 				pointsBySeries[metric] = series
 				seriesByQueryOrder.PushBack(metric)
@@ -288,7 +288,7 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
 
 			if fillMissing {
 				var intervalStart float64
-				if exist == false {
+				if !exist {
 					intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6)
 				} else {
 					intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval

+ 4 - 4
pkg/tsdb/postgres/macros.go

@@ -79,15 +79,15 @@ func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string,
 		}
 		return fmt.Sprintf("extract(epoch from %s) as \"time\"", args[0]), nil
 	case "__timeFilter":
-		// don't use to_timestamp in this macro for redshift compatibility #9566
 		if len(args) == 0 {
 			return "", fmt.Errorf("missing time column argument for macro %v", name)
 		}
-		return fmt.Sprintf("extract(epoch from %s) BETWEEN %d AND %d", args[0], m.TimeRange.GetFromAsSecondsEpoch(), m.TimeRange.GetToAsSecondsEpoch()), nil
+
+		return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
 	case "__timeFrom":
-		return fmt.Sprintf("to_timestamp(%d)", m.TimeRange.GetFromAsSecondsEpoch()), nil
+		return fmt.Sprintf("'%s'", m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil
 	case "__timeTo":
-		return fmt.Sprintf("to_timestamp(%d)", m.TimeRange.GetToAsSecondsEpoch()), nil
+		return fmt.Sprintf("'%s'", m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
 	case "__timeGroup":
 		if len(args) < 2 {
 			return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name)

+ 10 - 10
pkg/tsdb/postgres/macros_test.go

@@ -12,7 +12,7 @@ import (
 
 func TestMacroEngine(t *testing.T) {
 	Convey("MacroEngine", t, func() {
-		engine := &PostgresMacroEngine{}
+		engine := NewPostgresMacroEngine()
 		query := &tsdb.Query{}
 
 		Convey("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func() {
@@ -38,14 +38,14 @@ func TestMacroEngine(t *testing.T) {
 				sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, fmt.Sprintf("WHERE extract(epoch from time_column) BETWEEN %d AND %d", from.Unix(), to.Unix()))
+				So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
 			})
 
 			Convey("interpolate __timeFrom function", func() {
 				sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, fmt.Sprintf("select to_timestamp(%d)", from.Unix()))
+				So(sql, ShouldEqual, fmt.Sprintf("select '%s'", from.Format(time.RFC3339)))
 			})
 
 			Convey("interpolate __timeGroup function", func() {
@@ -68,7 +68,7 @@ func TestMacroEngine(t *testing.T) {
 				sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, fmt.Sprintf("select to_timestamp(%d)", to.Unix()))
+				So(sql, ShouldEqual, fmt.Sprintf("select '%s'", to.Format(time.RFC3339)))
 			})
 
 			Convey("interpolate __unixEpochFilter function", func() {
@@ -102,21 +102,21 @@ func TestMacroEngine(t *testing.T) {
 				sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, fmt.Sprintf("WHERE extract(epoch from time_column) BETWEEN %d AND %d", from.Unix(), to.Unix()))
+				So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
 			})
 
 			Convey("interpolate __timeFrom function", func() {
 				sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, fmt.Sprintf("select to_timestamp(%d)", from.Unix()))
+				So(sql, ShouldEqual, fmt.Sprintf("select '%s'", from.Format(time.RFC3339)))
 			})
 
 			Convey("interpolate __timeTo function", func() {
 				sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, fmt.Sprintf("select to_timestamp(%d)", to.Unix()))
+				So(sql, ShouldEqual, fmt.Sprintf("select '%s'", to.Format(time.RFC3339)))
 			})
 
 			Convey("interpolate __unixEpochFilter function", func() {
@@ -150,21 +150,21 @@ func TestMacroEngine(t *testing.T) {
 				sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, fmt.Sprintf("WHERE extract(epoch from time_column) BETWEEN %d AND %d", from.Unix(), to.Unix()))
+				So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
 			})
 
 			Convey("interpolate __timeFrom function", func() {
 				sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, fmt.Sprintf("select to_timestamp(%d)", from.Unix()))
+				So(sql, ShouldEqual, fmt.Sprintf("select '%s'", from.Format(time.RFC3339)))
 			})
 
 			Convey("interpolate __timeTo function", func() {
 				sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, fmt.Sprintf("select to_timestamp(%d)", to.Unix()))
+				So(sql, ShouldEqual, fmt.Sprintf("select '%s'", to.Format(time.RFC3339)))
 			})
 
 			Convey("interpolate __unixEpochFilter function", func() {

+ 5 - 5
pkg/tsdb/postgres/postgres.go

@@ -131,7 +131,7 @@ func (e PostgresQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues,
 	// convert types not handled by lib/pq
 	// unhandled types are returned as []byte
 	for i := 0; i < len(types); i++ {
-		if value, ok := values[i].([]byte); ok == true {
+		if value, ok := values[i].([]byte); ok {
 			switch types[i].DatabaseTypeName() {
 			case "NUMERIC":
 				if v, err := strconv.ParseFloat(string(value), 64); err == nil {
@@ -198,7 +198,7 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co
 	fillValue := null.Float{}
 	if fillMissing {
 		fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000
-		if query.Model.Get("fillNull").MustBool(false) == false {
+		if !query.Model.Get("fillNull").MustBool(false) {
 			fillValue.Float64 = query.Model.Get("fillValue").MustFloat64()
 			fillValue.Valid = true
 		}
@@ -233,7 +233,7 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co
 		}
 
 		if metricIndex >= 0 {
-			if columnValue, ok := values[metricIndex].(string); ok == true {
+			if columnValue, ok := values[metricIndex].(string); ok {
 				metric = columnValue
 			} else {
 				return fmt.Errorf("Column metric must be of type char,varchar or text, got: %T %v", values[metricIndex], values[metricIndex])
@@ -260,7 +260,7 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co
 			}
 
 			series, exist := pointsBySeries[metric]
-			if exist == false {
+			if !exist {
 				series = &tsdb.TimeSeries{Name: metric}
 				pointsBySeries[metric] = series
 				seriesByQueryOrder.PushBack(metric)
@@ -268,7 +268,7 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co
 
 			if fillMissing {
 				var intervalStart float64
-				if exist == false {
+				if !exist {
 					intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6)
 				} else {
 					intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval

+ 2 - 2
pkg/tsdb/prometheus/prometheus.go

@@ -108,8 +108,8 @@ func (e *PrometheusExecutor) Query(ctx context.Context, dsInfo *models.DataSourc
 
 		span, ctx := opentracing.StartSpanFromContext(ctx, "alerting.prometheus")
 		span.SetTag("expr", query.Expr)
-		span.SetTag("start_unixnano", int64(query.Start.UnixNano()))
-		span.SetTag("stop_unixnano", int64(query.End.UnixNano()))
+		span.SetTag("start_unixnano", query.Start.UnixNano())
+		span.SetTag("stop_unixnano", query.End.UnixNano())
 		defer span.Finish()
 
 		value, err := client.QueryRange(ctx, query.Expr, timeRange)

+ 1 - 1
pkg/tsdb/sql_engine.go

@@ -51,7 +51,7 @@ func (e *DefaultSqlEngine) InitEngine(driverName string, dsInfo *models.DataSour
 	defer engineCache.Unlock()
 
 	if engine, present := engineCache.cache[dsInfo.Id]; present {
-		if version, _ := engineCache.versions[dsInfo.Id]; version == dsInfo.Version {
+		if version := engineCache.versions[dsInfo.Id]; version == dsInfo.Version {
 			e.XormEngine = engine
 			return nil
 		}

+ 8 - 0
pkg/tsdb/time_range.go

@@ -37,6 +37,10 @@ func (tr *TimeRange) GetFromAsSecondsEpoch() int64 {
 	return tr.GetFromAsMsEpoch() / 1000
 }
 
+func (tr *TimeRange) GetFromAsTimeUTC() time.Time {
+	return tr.MustGetFrom().UTC()
+}
+
 func (tr *TimeRange) GetToAsMsEpoch() int64 {
 	return tr.MustGetTo().UnixNano() / int64(time.Millisecond)
 }
@@ -45,6 +49,10 @@ func (tr *TimeRange) GetToAsSecondsEpoch() int64 {
 	return tr.GetToAsMsEpoch() / 1000
 }
 
+func (tr *TimeRange) GetToAsTimeUTC() time.Time {
+	return tr.MustGetTo().UTC()
+}
+
 func (tr *TimeRange) MustGetFrom() time.Time {
 	if res, err := tr.ParseFrom(); err != nil {
 		return time.Unix(0, 0)

+ 1 - 5
pkg/util/shortid_generator.go

@@ -17,11 +17,7 @@ func init() {
 
 // IsValidShortUid checks if short unique identifier contains valid characters
 func IsValidShortUid(uid string) bool {
-	if !validUidPattern(uid) {
-		return false
-	}
-
-	return true
+	return validUidPattern(uid)
 }
 
 // GenerateShortUid generates a short unique identifier.

+ 2 - 2
public/app/core/components/Permissions/AddPermissions.tsx

@@ -39,7 +39,7 @@ class AddPermissions extends Component<IProps, any> {
       permissions.newItem.setUser(null, null);
       return;
     }
-    return permissions.newItem.setUser(user.id, user.login);
+    return permissions.newItem.setUser(user.id, user.login, user.avatarUrl);
   }
 
   teamPicked(team: Team) {
@@ -48,7 +48,7 @@ class AddPermissions extends Component<IProps, any> {
       permissions.newItem.setTeam(null, null);
       return;
     }
-    return permissions.newItem.setTeam(team.id, team.name);
+    return permissions.newItem.setTeam(team.id, team.name, team.avatarUrl);
   }
 
   permissionPicked(permission: OptionWithDescription) {

+ 7 - 4
public/app/core/components/Permissions/DisabledPermissionsListItem.tsx

@@ -1,4 +1,4 @@
-import React, { Component } from 'react';
+import React, { Component } from 'react';
 import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker';
 import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore';
 
@@ -12,9 +12,12 @@ export default class DisabledPermissionListItem extends Component<IProps, any> {
 
     return (
       <tr className="gf-form-disabled">
-        <td style={{ width: '100%' }}>
-          <i className={`fa--permissions-list ${item.icon}`} />
-          <span dangerouslySetInnerHTML={{ __html: item.nameHtml }} />
+        <td style={{ width: '1%' }}>
+          <i style={{ width: '25px', height: '25px' }} className="gicon gicon-shield" />
+        </td>
+        <td style={{ width: '90%' }}>
+          {item.name}
+          <span className="filter-table__weak-italic"> (Role)</span>
         </td>
         <td />
         <td className="query-keyword">Can</td>

+ 1 - 2
public/app/core/components/Permissions/Permissions.tsx

@@ -15,9 +15,8 @@ export interface DashboardAcl {
   permissionName?: string;
   role?: string;
   icon?: string;
-  nameHtml?: string;
+  name?: string;
   inherited?: boolean;
-  sortName?: string;
   sortRank?: number;
 }
 

+ 2 - 2
public/app/core/components/Permissions/PermissionsList.tsx

@@ -1,4 +1,4 @@
-import React, { Component } from 'react';
+import React, { Component } from 'react';
 import PermissionsListItem from './PermissionsListItem';
 import DisabledPermissionsListItem from './DisabledPermissionsListItem';
 import { observer } from 'mobx-react';
@@ -23,7 +23,7 @@ class PermissionsList extends Component<IProps, any> {
           <DisabledPermissionsListItem
             key={0}
             item={{
-              nameHtml: 'Everyone with <span class="query-keyword">Admin</span> Role',
+              name: 'Admin',
               permission: 4,
               icon: 'fa fa-fw fa-street-view',
             }}

+ 30 - 4
public/app/core/components/Permissions/PermissionsListItem.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React from 'react';
 import { observer } from 'mobx-react';
 import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker';
 import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore';
@@ -7,6 +7,30 @@ const setClassNameHelper = inherited => {
   return inherited ? 'gf-form-disabled' : '';
 };
 
+function ItemAvatar({ item }) {
+  if (item.userAvatarUrl) {
+    return <img className="filter-table__avatar" src={item.userAvatarUrl} />;
+  }
+  if (item.teamAvatarUrl) {
+    return <img className="filter-table__avatar" src={item.teamAvatarUrl} />;
+  }
+  if (item.role === 'Editor') {
+    return <i style={{ width: '25px', height: '25px' }} className="gicon gicon-editor" />;
+  }
+
+  return <i style={{ width: '25px', height: '25px' }} className="gicon gicon-viewer" />;
+}
+
+function ItemDescription({ item }) {
+  if (item.userId) {
+    return <span className="filter-table__weak-italic">(User)</span>;
+  }
+  if (item.teamId) {
+    return <span className="filter-table__weak-italic">(Team)</span>;
+  }
+  return <span className="filter-table__weak-italic">(Role)</span>;
+}
+
 export default observer(({ item, removeItem, permissionChanged, itemIndex, folderInfo }) => {
   const handleRemoveItem = evt => {
     evt.preventDefault();
@@ -21,9 +45,11 @@ export default observer(({ item, removeItem, permissionChanged, itemIndex, folde
 
   return (
     <tr className={setClassNameHelper(item.inherited)}>
-      <td style={{ width: '100%' }}>
-        <i className={`fa--permissions-list ${item.icon}`} />
-        <span dangerouslySetInnerHTML={{ __html: item.nameHtml }} />
+      <td style={{ width: '1%' }}>
+        <ItemAvatar item={item} />
+      </td>
+      <td style={{ width: '90%' }}>
+        {item.name} <ItemDescription item={item} />
       </td>
       <td>
         {item.inherited &&

+ 1 - 1
public/app/core/components/sidemenu/sidemenu.html

@@ -37,7 +37,7 @@
         <i class="fa fa-fw fa-sign-in"></i>
       </span>
     </a>
-    <a href="{{ctrl.loginUrl}}">
+    <a href="{{ctrl.loginUrl}}" target="_self">
       <ul class="dropdown-menu dropdown-menu--sidemenu" role="menu">
         <li class="side-menu-header">
           <span class="sidemenu-item-text">Sign In</span>

+ 1 - 0
public/app/features/dashboard/all.ts

@@ -6,6 +6,7 @@ import './dashnav/dashnav';
 import './submenu/submenu';
 import './save_as_modal';
 import './save_modal';
+import './save_provisioned_modal';
 import './shareModalCtrl';
 import './share_snapshot_ctrl';
 import './dashboard_srv';

+ 186 - 0
public/app/features/dashboard/change_tracker.ts

@@ -0,0 +1,186 @@
+import angular from 'angular';
+import _ from 'lodash';
+import { DashboardModel } from './dashboard_model';
+
+export class ChangeTracker {
+  current: any;
+  originalPath: any;
+  scope: any;
+  original: any;
+  next: any;
+  $window: any;
+
+  /** @ngInject */
+  constructor(
+    dashboard,
+    scope,
+    originalCopyDelay,
+    private $location,
+    $window,
+    private $timeout,
+    private contextSrv,
+    private $rootScope
+  ) {
+    this.$location = $location;
+    this.$window = $window;
+
+    this.current = dashboard;
+    this.originalPath = $location.path();
+    this.scope = scope;
+
+    // register events
+    scope.onAppEvent('dashboard-saved', () => {
+      this.original = this.current.getSaveModelClone();
+      this.originalPath = $location.path();
+    });
+
+    $window.onbeforeunload = () => {
+      if (this.ignoreChanges()) {
+        return undefined;
+      }
+      if (this.hasChanges()) {
+        return 'There are unsaved changes to this dashboard';
+      }
+      return undefined;
+    };
+
+    scope.$on('$locationChangeStart', (event, next) => {
+      // check if we should look for changes
+      if (this.originalPath === $location.path()) {
+        return true;
+      }
+      if (this.ignoreChanges()) {
+        return true;
+      }
+
+      if (this.hasChanges()) {
+        event.preventDefault();
+        this.next = next;
+
+        this.$timeout(() => {
+          this.open_modal();
+        });
+      }
+      return false;
+    });
+
+    if (originalCopyDelay) {
+      this.$timeout(() => {
+        // wait for different services to patch the dashboard (missing properties)
+        this.original = dashboard.getSaveModelClone();
+      }, originalCopyDelay);
+    } else {
+      this.original = dashboard.getSaveModelClone();
+    }
+  }
+
+  // for some dashboards and users
+  // changes should be ignored
+  ignoreChanges() {
+    if (!this.original) {
+      return true;
+    }
+    if (!this.contextSrv.isEditor) {
+      return true;
+    }
+    if (!this.current || !this.current.meta) {
+      return true;
+    }
+
+    var meta = this.current.meta;
+    return !meta.canSave || meta.fromScript || meta.fromFile;
+  }
+
+  // remove stuff that should not count in diff
+  cleanDashboardFromIgnoredChanges(dashData) {
+    // need to new up the domain model class to get access to expand / collapse row logic
+    let model = new DashboardModel(dashData);
+
+    // Expand all rows before making comparison. This is required because row expand / collapse
+    // change order of panel array and panel positions.
+    model.expandRows();
+
+    let dash = model.getSaveModelClone();
+
+    // ignore time and refresh
+    dash.time = 0;
+    dash.refresh = 0;
+    dash.schemaVersion = 0;
+
+    // ignore iteration property
+    delete dash.iteration;
+
+    dash.panels = _.filter(dash.panels, panel => {
+      if (panel.repeatPanelId) {
+        return false;
+      }
+
+      // remove scopedVars
+      panel.scopedVars = null;
+
+      // ignore panel legend sort
+      if (panel.legend) {
+        delete panel.legend.sort;
+        delete panel.legend.sortDesc;
+      }
+
+      return true;
+    });
+
+    // ignore template variable values
+    _.each(dash.templating.list, function(value) {
+      value.current = null;
+      value.options = null;
+      value.filters = null;
+    });
+
+    return dash;
+  }
+
+  hasChanges() {
+    let current = this.cleanDashboardFromIgnoredChanges(this.current.getSaveModelClone());
+    let original = this.cleanDashboardFromIgnoredChanges(this.original);
+
+    var currentTimepicker = _.find(current.nav, { type: 'timepicker' });
+    var originalTimepicker = _.find(original.nav, { type: 'timepicker' });
+
+    if (currentTimepicker && originalTimepicker) {
+      currentTimepicker.now = originalTimepicker.now;
+    }
+
+    var currentJson = angular.toJson(current, true);
+    var originalJson = angular.toJson(original, true);
+
+    return currentJson !== originalJson;
+  }
+
+  discardChanges() {
+    this.original = null;
+    this.gotoNext();
+  }
+
+  open_modal() {
+    this.$rootScope.appEvent('show-modal', {
+      templateHtml: '<unsaved-changes-modal dismiss="dismiss()"></unsaved-changes-modal>',
+      modalClass: 'modal--narrow confirm-modal',
+    });
+  }
+
+  saveChanges() {
+    var self = this;
+    var cancel = this.$rootScope.$on('dashboard-saved', () => {
+      cancel();
+      this.$timeout(() => {
+        self.gotoNext();
+      });
+    });
+
+    this.$rootScope.appEvent('save-dashboard');
+  }
+
+  gotoNext() {
+    var baseLen = this.$location.absUrl().length - this.$location.url().length;
+    var nextUrl = this.next.substring(baseLen);
+    this.$location.url(nextUrl);
+  }
+}

+ 2 - 1
public/app/features/dashboard/dashboard_model.ts

@@ -649,6 +649,7 @@ export class DashboardModel {
 
         for (let panel of row.panels) {
           // make sure y is adjusted (in case row moved while collapsed)
+          // console.log('yDiff', yDiff);
           panel.gridPos.y -= yDiff;
           // insert after row
           this.panels.splice(insertPos, 0, new PanelModel(panel));
@@ -657,7 +658,7 @@ export class DashboardModel {
           yMax = Math.max(yMax, panel.gridPos.y + panel.gridPos.h);
         }
 
-        const pushDownAmount = yMax - row.gridPos.y;
+        const pushDownAmount = yMax - row.gridPos.y - 1;
 
         // push panels below down
         for (let panelIndex = insertPos; panelIndex < this.panels.length; panelIndex++) {

+ 10 - 0
public/app/features/dashboard/dashboard_srv.ts

@@ -105,6 +105,10 @@ export class DashboardSrv {
       this.setCurrent(this.create(clone, this.dash.meta));
     }
 
+    if (this.dash.meta.provisioned) {
+      return this.showDashboardProvisionedModal();
+    }
+
     if (!this.dash.meta.canSave && options.makeEditable !== true) {
       return Promise.resolve();
     }
@@ -120,6 +124,12 @@ export class DashboardSrv {
     return this.save(this.dash.getSaveModelClone(), options);
   }
 
+  showDashboardProvisionedModal() {
+    this.$rootScope.appEvent('show-modal', {
+      templateHtml: '<save-provisioned-dashboard-modal dismiss="dismiss()"></save-provisioned-dashboard-modal>',
+    });
+  }
+
   showSaveAsModal() {
     this.$rootScope.appEvent('show-modal', {
       templateHtml: '<save-dashboard-as-modal dismiss="dismiss()"></save-dashboard-as-modal>',

+ 14 - 10
public/app/features/dashboard/folder_picker/folder_picker.ts

@@ -19,9 +19,12 @@ export class FolderPickerCtrl {
   newFolderNameTouched: boolean;
   hasValidationError: boolean;
   validationError: any;
+  isEditor: boolean;
 
   /** @ngInject */
-  constructor(private backendSrv, private validationSrv) {
+  constructor(private backendSrv, private validationSrv, private contextSrv) {
+    this.isEditor = this.contextSrv.isEditor;
+
     if (!this.labelClass) {
       this.labelClass = 'width-7';
     }
@@ -38,19 +41,20 @@ export class FolderPickerCtrl {
 
     return this.backendSrv.get('api/search', params).then(result => {
       if (
-        query === '' ||
-        query.toLowerCase() === 'g' ||
-        query.toLowerCase() === 'ge' ||
-        query.toLowerCase() === 'gen' ||
-        query.toLowerCase() === 'gene' ||
-        query.toLowerCase() === 'gener' ||
-        query.toLowerCase() === 'genera' ||
-        query.toLowerCase() === 'general'
+        this.isEditor &&
+        (query === '' ||
+          query.toLowerCase() === 'g' ||
+          query.toLowerCase() === 'ge' ||
+          query.toLowerCase() === 'gen' ||
+          query.toLowerCase() === 'gene' ||
+          query.toLowerCase() === 'gener' ||
+          query.toLowerCase() === 'genera' ||
+          query.toLowerCase() === 'general')
       ) {
         result.unshift({ title: this.rootName, id: 0 });
       }
 
-      if (this.enableCreateNew && query === '') {
+      if (this.isEditor && this.enableCreateNew && query === '') {
         result.unshift({ title: '-- New Folder --', id: -1 });
       }
 

+ 77 - 0
public/app/features/dashboard/save_provisioned_modal.ts

@@ -0,0 +1,77 @@
+import angular from 'angular';
+import { saveAs } from 'file-saver';
+import coreModule from 'app/core/core_module';
+
+const template = `
+<div class="modal-body">
+  <div class="modal-header">
+    <h2 class="modal-header-title">
+      <i class="fa fa-save"></i><span class="p-l-1">Cannot save provisioned dashboard</span>
+    </h2>
+
+    <a class="modal-header-close" ng-click="ctrl.dismiss();">
+      <i class="fa fa-remove"></i>
+    </a>
+  </div>
+
+  <div class="modal-content">
+    <small>
+      This dashboard cannot be saved from Grafana's UI since it has been provisioned from another source.
+      Copy the JSON or save it to a file below. Then you can update your dashboard in corresponding provisioning source.<br/>
+      <i>See <a class="external-link" href="http://docs.grafana.org/administration/provisioning/#dashboards" target="_blank">
+      documentation</a> for more information about provisioning.</i>
+    </small>
+    <div class="p-t-2">
+      <div class="gf-form">
+        <code-editor content="ctrl.dashboardJson" data-mode="json" data-max-lines=15></code-editor>
+      </div>
+      <div class="gf-form-button-row">
+        <button class="btn btn-success" clipboard-button="ctrl.getJsonForClipboard()">
+          <i class="fa fa-clipboard"></i>&nbsp;Copy JSON to Clipboard
+        </button>
+        <button class="btn btn-secondary" clipboard-button="ctrl.save()">
+          <i class="fa fa-save"></i>&nbsp;Save JSON to file
+        </button>
+        <a class="btn btn-link" ng-click="ctrl.dismiss();">Cancel</a>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+export class SaveProvisionedDashboardModalCtrl {
+  dash: any;
+  dashboardJson: string;
+  dismiss: () => void;
+
+  /** @ngInject */
+  constructor(dashboardSrv) {
+    this.dash = dashboardSrv.getCurrent().getSaveModelClone();
+    delete this.dash.id;
+    this.dashboardJson = JSON.stringify(this.dash, null, 2);
+  }
+
+  save() {
+    var blob = new Blob([angular.toJson(this.dash, true)], {
+      type: 'application/json;charset=utf-8',
+    });
+    saveAs(blob, this.dash.title + '-' + new Date().getTime() + '.json');
+  }
+
+  getJsonForClipboard() {
+    return this.dashboardJson;
+  }
+}
+
+export function saveProvisionedDashboardModalDirective() {
+  return {
+    restrict: 'E',
+    template: template,
+    controller: SaveProvisionedDashboardModalCtrl,
+    bindToController: true,
+    controllerAs: 'ctrl',
+    scope: { dismiss: '&' },
+  };
+}
+
+coreModule.directive('saveProvisionedDashboardModal', saveProvisionedDashboardModalDirective);

Некоторые файлы не были показаны из-за большого количества измененных файлов