浏览代码

Merge branch 'master' into docs_v5.0

Torkel Ödegaard 7 年之前
父节点
当前提交
2f6e77af1e
共有 100 个文件被更改,包括 4595 次插入2069 次删除
  1. 1 1
      .jshintrc
  2. 31 1
      CHANGELOG.md
  3. 1 1
      conf/defaults.ini
  4. 1 1
      conf/ldap.toml
  5. 5 1
      conf/provisioning/dashboards/sample.yaml
  6. 15 12
      conf/provisioning/datasources/sample.yaml
  7. 1 1
      conf/sample.ini
  8. 549 0
      docker/blocks/mysql/dashboard.json
  9. 547 0
      docker/blocks/postgres/dashboard.json
  10. 30 15
      docs/sources/administration/provisioning.md
  11. 1 1
      docs/sources/features/datasources/cloudwatch.md
  12. 1 1
      docs/sources/guides/whats-new-in-v5.md
  13. 1 1
      docs/sources/http_api/dashboard.md
  14. 3 3
      docs/sources/installation/debian.md
  15. 1 1
      docs/sources/installation/ldap.md
  16. 2 2
      docs/sources/installation/rpm.md
  17. 4 1
      docs/sources/installation/upgrading.md
  18. 1 1
      docs/sources/installation/windows.md
  19. 29 156
      docs/sources/reference/dashboard.md
  20. 4 1
      package.json
  21. 2 2
      packaging/publish/publish_testing.sh
  22. 5 67
      pkg/api/alerting.go
  23. 1 1
      pkg/api/annotations.go
  24. 0 3
      pkg/api/api.go
  25. 21 58
      pkg/api/dashboard.go
  26. 2 32
      pkg/api/dashboard_acl.go
  27. 9 95
      pkg/api/dashboard_acl_test.go
  28. 116 116
      pkg/api/dashboard_test.go
  29. 1 1
      pkg/api/plugins.go
  30. 7 0
      pkg/api/search.go
  31. 14 1
      pkg/api/team_members.go
  32. 1 1
      pkg/cmd/grafana-cli/commands/install_command.go
  33. 2 1
      pkg/middleware/auth.go
  34. 4 4
      pkg/middleware/recovery.go
  35. 31 1
      pkg/models/alert.go
  36. 0 16
      pkg/models/dashboard_acl.go
  37. 72 28
      pkg/models/dashboards.go
  38. 16 15
      pkg/models/datasource.go
  39. 1 1
      pkg/models/login_attempt.go
  40. 3 2
      pkg/models/team.go
  41. 18 8
      pkg/plugins/dashboard_importer.go
  42. 12 13
      pkg/plugins/dashboard_importer_test.go
  43. 1 1
      pkg/plugins/dashboards_updater.go
  44. 2 14
      pkg/services/alerting/commands.go
  45. 232 0
      pkg/services/dashboards/dashboard_service.go
  46. 144 0
      pkg/services/dashboards/dashboard_service_test.go
  47. 0 82
      pkg/services/dashboards/dashboards.go
  48. 24 32
      pkg/services/guardian/guardian.go
  49. 43 10
      pkg/services/provisioning/dashboards/config_reader.go
  50. 37 29
      pkg/services/provisioning/dashboards/config_reader_test.go
  51. 0 33
      pkg/services/provisioning/dashboards/dashboard_cache.go
  52. 191 82
      pkg/services/provisioning/dashboards/file_reader.go
  53. 43 48
      pkg/services/provisioning/dashboards/file_reader_test.go
  54. 5 1
      pkg/services/provisioning/dashboards/test-configs/dashboards-from-disk/dev-dashboards.yaml
  55. 10 0
      pkg/services/provisioning/dashboards/test-configs/dashboards-from-disk/sample.yaml
  56. 13 0
      pkg/services/provisioning/dashboards/test-configs/version-0/version-0.yaml
  57. 1 2
      pkg/services/provisioning/dashboards/test-dashboards/folder-one/dashboard1.json
  58. 1 2
      pkg/services/provisioning/dashboards/test-dashboards/folder-one/dashboard2.json
  59. 73 8
      pkg/services/provisioning/dashboards/types.go
  60. 113 0
      pkg/services/provisioning/datasources/config_reader.go
  61. 70 31
      pkg/services/provisioning/datasources/config_reader_test.go
  62. 0 66
      pkg/services/provisioning/datasources/datasources.go
  63. 15 8
      pkg/services/provisioning/datasources/test-configs/all-properties/all-properties.yaml
  64. 32 0
      pkg/services/provisioning/datasources/test-configs/all-properties/sample.yaml
  65. 1 1
      pkg/services/provisioning/datasources/test-configs/all-properties/second.yaml
  66. 28 0
      pkg/services/provisioning/datasources/test-configs/version-0/version-0.yaml
  67. 160 6
      pkg/services/provisioning/datasources/types.go
  68. 1 0
      pkg/services/search/handlers.go
  69. 2 1
      pkg/services/search/models.go
  70. 33 24
      pkg/services/sqlstore/alert.go
  71. 13 7
      pkg/services/sqlstore/alert_test.go
  72. 199 221
      pkg/services/sqlstore/dashboard.go
  73. 20 127
      pkg/services/sqlstore/dashboard_acl.go
  74. 27 64
      pkg/services/sqlstore/dashboard_acl_test.go
  75. 51 40
      pkg/services/sqlstore/dashboard_folder_test.go
  76. 66 0
      pkg/services/sqlstore/dashboard_provisioning.go
  77. 55 0
      pkg/services/sqlstore/dashboard_provisioning_test.go
  78. 984 0
      pkg/services/sqlstore/dashboard_service_integration_test.go
  79. 40 341
      pkg/services/sqlstore/dashboard_test.go
  80. 1 1
      pkg/services/sqlstore/dashboard_version_test.go
  81. 3 0
      pkg/services/sqlstore/datasource.go
  82. 0 48
      pkg/services/sqlstore/datasource_test.go
  83. 4 4
      pkg/services/sqlstore/login_attempt.go
  84. 21 0
      pkg/services/sqlstore/migrations/common.go
  85. 38 1
      pkg/services/sqlstore/migrations/dashboard_mig.go
  86. 19 0
      pkg/services/sqlstore/migrations/login_attempt_mig.go
  87. 23 20
      pkg/services/sqlstore/migrations/migrations_test.go
  88. 4 0
      pkg/services/sqlstore/migrator/migrator.go
  89. 13 2
      pkg/services/sqlstore/org_test.go
  90. 0 4
      pkg/services/sqlstore/playlist.go
  91. 6 25
      pkg/services/sqlstore/search_builder.go
  92. 2 1
      pkg/services/sqlstore/search_builder_test.go
  93. 75 0
      pkg/services/sqlstore/sqlbuilder.go
  94. 45 2
      pkg/services/sqlstore/sqlstore.go
  95. 2 2
      pkg/services/sqlstore/sqlutil/sqlutil.go
  96. 30 5
      pkg/services/sqlstore/team.go
  97. 7 4
      pkg/services/sqlstore/team_test.go
  98. 1 1
      pkg/services/sqlstore/user_test.go
  99. 1 1
      pkg/social/github_oauth.go
  100. 4 0
      pkg/tsdb/cloudwatch/metric_find_query.go

+ 1 - 1
.jshintrc

@@ -4,7 +4,7 @@
   "bitwise":false,
   "bitwise":false,
   "curly": true,
   "curly": true,
   "eqnull": true,
   "eqnull": true,
-  "strict": true,
+  "strict": false,
   "devel": true,
   "devel": true,
   "eqeqeq": true,
   "eqeqeq": true,
   "forin": false,
   "forin": false,

+ 31 - 1
CHANGELOG.md

@@ -1,4 +1,34 @@
-# 5.0.0-beta2 (unrelased)
+# 5.0.0-beta4 (2018-02-19)
+
+### Fixes
+
+- **Dashboard** Fixed dashboard overwrite permission issue [#10814](https://github.com/grafana/grafana/issues/10814)
+- **Keyboard shortcuts** Fixed Esc key when in panel edit/view mode [#10945](https://github.com/grafana/grafana/issues/10945)
+- **Save dashboard** Fixed issue with time range & variable reset after saving [#10946](https://github.com/grafana/grafana/issues/10946)
+
+# 5.0.0-beta3 (2018-02-16)
+
+### Fixes
+
+- **MySQL** Fixed new migration issue with index length [#10931](https://github.com/grafana/grafana/issues/10931)
+- **Modal** Escape key no closes modals everywhere, fixes [#10887](https://github.com/grafana/grafana/issues/10887)
+- **Row repeats** Fix for repeating rows issue, fixes [#10932](https://github.com/grafana/grafana/issues/10932)
+- **Docs** Team api documented, fixes [#10832](https://github.com/grafana/grafana/issues/10832)
+- **Plugins** Plugin info page broken, fixes [#10943](https://github.com/grafana/grafana/issues/10943)
+
+# 5.0.0-beta2 (2018-02-15)
+
+### Fixes
+
+- **Permissions** Fixed search permissions issues [#10822](https://github.com/grafana/grafana/issues/10822)
+- **Permissions** Fixed problem issues displaying permissions lists [#10864](https://github.com/grafana/grafana/issues/10864)
+- **PNG-Rendering** Fixed problem rendering legend to the right [#10526](https://github.com/grafana/grafana/issues/10526)
+- **Reset password** Fixed problem with reset password form [#10870](https://github.com/grafana/grafana/issues/10870)
+- **Light theme** Fixed problem with light theme in safari, [#10869](https://github.com/grafana/grafana/issues/10869)
+- **Provisioning** Now handles deletes when dashboard json files removed from disk [#10865](https://github.com/grafana/grafana/issues/10865)
+- **MySQL** Fixed issue with schema migration on old mysql (index too long) [#10779](https://github.com/grafana/grafana/issues/10779)
+- **Github OAuth** Fixed fetching github orgs from private github org [#10823](https://github.com/grafana/grafana/issues/10823)
+- **Embedding** Fixed issues embedding panel [#10787](https://github.com/grafana/grafana/issues/10787)
 
 
 # 5.0.0-beta1 (2018-02-05)
 # 5.0.0-beta1 (2018-02-05)
 
 

+ 1 - 1
conf/defaults.ini

@@ -327,7 +327,7 @@ allow_sign_up = true
 enabled = false
 enabled = false
 host = localhost:25
 host = localhost:25
 user =
 user =
-# If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;"""
+# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;"""
 password =
 password =
 cert_file =
 cert_file =
 key_file =
 key_file =

+ 1 - 1
conf/ldap.toml

@@ -19,7 +19,7 @@ ssl_skip_verify = false
 # Search user bind dn
 # Search user bind dn
 bind_dn = "cn=admin,dc=grafana,dc=org"
 bind_dn = "cn=admin,dc=grafana,dc=org"
 # Search user bind password
 # Search user bind password
-# If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;"""
+# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;"""
 bind_password = 'grafana'
 bind_password = 'grafana'
 
 
 # User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"
 # User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"

+ 5 - 1
conf/provisioning/dashboards/sample.yaml

@@ -1,5 +1,9 @@
+# # config file version
+apiVersion: 1
+
+#providers:
 # - name: 'default'
 # - name: 'default'
-#   org_id: 1
+#   orgId: 1
 #   folder: ''
 #   folder: ''
 #   type: file
 #   type: file
 #   options:
 #   options:

+ 15 - 12
conf/provisioning/datasources/sample.yaml

@@ -1,10 +1,13 @@
+# # config file version
+apiVersion: 1
+
 # # list of datasources that should be deleted from the database
 # # list of datasources that should be deleted from the database
-#delete_datasources:
+#deleteDatasources:
 #   - name: Graphite
 #   - name: Graphite
-#     org_id: 1
+#     orgId: 1
 
 
 # # list of datasources to insert/update depending
 # # list of datasources to insert/update depending
-# # whats available in the datbase
+# # on what's available in the datbase
 #datasources:
 #datasources:
 #   # <string, required> name of the datasource. Required
 #   # <string, required> name of the datasource. Required
 # - name: Graphite
 # - name: Graphite
@@ -12,8 +15,8 @@
 #   type: graphite
 #   type: graphite
 #   # <string, required> access mode. direct or proxy. Required
 #   # <string, required> access mode. direct or proxy. Required
 #   access: proxy
 #   access: proxy
-#   # <int> org id. will default to org_id 1 if not specified
-#   org_id: 1
+#   # <int> org id. will default to orgId 1 if not specified
+#   orgId: 1
 #   # <string> url
 #   # <string> url
 #   url: http://localhost:8080
 #   url: http://localhost:8080
 #   # <string> database password, if used
 #   # <string> database password, if used
@@ -23,22 +26,22 @@
 #   # <string> database name, if used
 #   # <string> database name, if used
 #   database:
 #   database:
 #   # <bool> enable/disable basic auth
 #   # <bool> enable/disable basic auth
-#   basic_auth:
+#   basicAuth:
 #   # <string> basic auth username
 #   # <string> basic auth username
-#   basic_auth_user:
+#   basicAuthUser:
 #   # <string> basic auth password
 #   # <string> basic auth password
-#   basic_auth_password:
+#   basicAuthPassword:
 #   # <bool> enable/disable with credentials headers
 #   # <bool> enable/disable with credentials headers
-#   with_credentials:
+#   withCredentials:
 #   # <bool> mark as default datasource. Max one per org
 #   # <bool> mark as default datasource. Max one per org
-#   is_default:
+#   isDefault:
 #   # <map> fields that will be converted to json and stored in json_data
 #   # <map> fields that will be converted to json and stored in json_data
-#   json_data:
+#   jsonData:
 #      graphiteVersion: "1.1"
 #      graphiteVersion: "1.1"
 #      tlsAuth: true
 #      tlsAuth: true
 #      tlsAuthWithCACert: true
 #      tlsAuthWithCACert: true
 #   # <string> json object of data that will be encrypted.
 #   # <string> json object of data that will be encrypted.
-#   secure_json_data:
+#   secureJsonData:
 #     tlsCACert: "..."
 #     tlsCACert: "..."
 #     tlsClientCert: "..."
 #     tlsClientCert: "..."
 #     tlsClientKey: "..."
 #     tlsClientKey: "..."

+ 1 - 1
conf/sample.ini

@@ -71,7 +71,7 @@
 ;host = 127.0.0.1:3306
 ;host = 127.0.0.1:3306
 ;name = grafana
 ;name = grafana
 ;user = root
 ;user = root
-# If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;"""
+# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;"""
 ;password =
 ;password =
 
 
 # Use either URL or the previous fields to configure the database
 # Use either URL or the previous fields to configure the database

+ 549 - 0
docker/blocks/mysql/dashboard.json

@@ -0,0 +1,549 @@
+{
+  "__inputs": [
+    {
+      "name": "DS_MYSQL",
+      "label": "Mysql",
+      "description": "",
+      "type": "datasource",
+      "pluginId": "mysql",
+      "pluginName": "MySQL"
+    }
+  ],
+  "__requires": [
+    {
+      "type": "grafana",
+      "id": "grafana",
+      "name": "Grafana",
+      "version": "5.0.0"
+    },
+    {
+      "type": "panel",
+      "id": "graph",
+      "name": "Graph",
+      "version": ""
+    },
+    {
+      "type": "datasource",
+      "id": "mysql",
+      "name": "MySQL",
+      "version": "1.0.0"
+    },
+    {
+      "type": "panel",
+      "id": "table",
+      "name": "Table",
+      "version": ""
+    }
+  ],
+  "annotations": {
+    "list": [
+      {
+        "builtIn": 1,
+        "datasource": "-- Grafana --",
+        "enable": true,
+        "hide": true,
+        "iconColor": "rgba(0, 211, 255, 1)",
+        "name": "Annotations & Alerts",
+        "type": "dashboard"
+      }
+    ]
+  },
+  "description": "A dashboard visualizing data generated from grafana/fake-data-gen",
+  "editable": true,
+  "gnetId": null,
+  "graphTooltip": 0,
+  "id": null,
+  "iteration": 1518602729468,
+  "links": [],
+  "panels": [
+    {
+      "aliasColors": {
+        "total avg": "#6ed0e0"
+      },
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_MYSQL}",
+      "fill": 2,
+      "gridPos": {
+        "h": 9,
+        "w": 12,
+        "x": 0,
+        "y": 0
+      },
+      "id": 2,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [
+        {
+          "alias": "total avg",
+          "fill": 0,
+          "pointradius": 3,
+          "points": true
+        }
+      ],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "hide": false,
+          "rawSql": "SELECT\n  $__timeGroup(createdAt,'$summarize') as time_sec,\n  avg(value) as value,\n  hostname as metric\nFROM \n  grafana_metric\nWHERE\n  $__timeFilter(createdAt) AND\n  measurement = 'logins.count' AND\n  hostname IN($host)\nGROUP BY 1, 3\nORDER BY 1",
+          "refId": "A",
+          "target": ""
+        },
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT\n  $__timeGroup(createdAt,'$summarize') as time_sec,\n  min(value) as value,\n  'total avg' as metric\nFROM \n  grafana_metric\nWHERE\n  $__timeFilter(createdAt) AND\n  measurement = 'logins.count'\nGROUP BY 1\nORDER BY 1",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": "1h",
+      "title": "Average logins / $summarize",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_MYSQL}",
+      "fill": 2,
+      "gridPos": {
+        "h": 18,
+        "w": 12,
+        "x": 12,
+        "y": 0
+      },
+      "id": 4,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT\n  $__timeGroup(createdAt,'$summarize') as time_sec,\n  avg(value) as value,\n  'started' as metric\nFROM \n  grafana_metric\nWHERE\n  $__timeFilter(createdAt) AND\n  measurement = 'payment.started'\nGROUP BY 1, 3\nORDER BY 1",
+          "refId": "A",
+          "target": ""
+        },
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT\n  $__timeGroup(createdAt,'$summarize') as time_sec,\n  avg(value) as value,\n  'ended' as \"metric\"\nFROM \n  grafana_metric\nWHERE\n  $__timeFilter(createdAt) AND\n  measurement = 'payment.ended'\nGROUP BY 1, 3\nORDER BY 1",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": "1h",
+      "title": "Average payments started/ended / $summarize",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_MYSQL}",
+      "fill": 2,
+      "gridPos": {
+        "h": 9,
+        "w": 12,
+        "x": 0,
+        "y": 9
+      },
+      "id": 3,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT\n  $__timeGroup(createdAt,'$summarize') as time_sec,\n  max(value) as value,\n  hostname as metric\nFROM \n  grafana_metric\nWHERE\n  $__timeFilter(createdAt) AND\n  measurement = 'cpu' AND\n  hostname IN($host)\nGROUP BY 1, 3\nORDER BY 1",
+          "refId": "A",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": "1h",
+      "title": "Max CPU / $summarize",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "percent",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "columns": [],
+      "datasource": "${DS_MYSQL}",
+      "fontSize": "100%",
+      "gridPos": {
+        "h": 9,
+        "w": 24,
+        "x": 0,
+        "y": 18
+      },
+      "id": 6,
+      "links": [],
+      "pageSize": null,
+      "scroll": true,
+      "showHeader": true,
+      "sort": {
+        "col": 0,
+        "desc": true
+      },
+      "styles": [
+        {
+          "alias": "Time",
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "link": false,
+          "pattern": "Time",
+          "type": "date"
+        },
+        {
+          "alias": "",
+          "colorMode": null,
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "decimals": 2,
+          "pattern": "/.*/",
+          "thresholds": [],
+          "type": "number",
+          "unit": "short"
+        }
+      ],
+      "targets": [
+        {
+          "alias": "",
+          "format": "table",
+          "rawSql": "SELECT createdAt as Time, source, datacenter, hostname, value FROM grafana_metric WHERE hostname in($host)",
+          "refId": "A",
+          "target": ""
+        }
+      ],
+      "timeShift": "1h",
+      "title": "Values",
+      "transform": "table",
+      "type": "table"
+    }
+  ],
+  "schemaVersion": 16,
+  "style": "dark",
+  "tags": [
+    "fake-data-gen",
+    "mysql"
+  ],
+  "templating": {
+    "list": [
+      {
+        "allValue": null,
+        "current": {},
+        "datasource": "${DS_MYSQL}",
+        "hide": 0,
+        "includeAll": false,
+        "label": "Datacenter",
+        "multi": false,
+        "name": "datacenter",
+        "options": [],
+        "query": "SELECT DISTINCT datacenter FROM grafana_metric",
+        "refresh": 1,
+        "regex": "",
+        "sort": 1,
+        "tagValuesQuery": "",
+        "tags": [],
+        "tagsQuery": "",
+        "type": "query",
+        "useTags": false
+      },
+      {
+        "allValue": null,
+        "current": {},
+        "datasource": "${DS_MYSQL}",
+        "hide": 0,
+        "includeAll": true,
+        "label": "Hostname",
+        "multi": true,
+        "name": "host",
+        "options": [],
+        "query": "SELECT DISTINCT hostname FROM grafana_metric WHERE datacenter='$datacenter'",
+        "refresh": 1,
+        "regex": "",
+        "sort": 1,
+        "tagValuesQuery": "",
+        "tags": [],
+        "tagsQuery": "",
+        "type": "query",
+        "useTags": false
+      },
+      {
+        "auto": false,
+        "auto_count": 5,
+        "auto_min": "10s",
+        "current": {
+          "selected": true,
+          "text": "1m",
+          "value": "1m"
+        },
+        "hide": 0,
+        "label": "Summarize",
+        "name": "summarize",
+        "options": [
+          {
+            "selected": false,
+            "text": "1s",
+            "value": "1s"
+          },
+          {
+            "selected": false,
+            "text": "10s",
+            "value": "10s"
+          },
+          {
+            "selected": false,
+            "text": "30s",
+            "value": "30s"
+          },
+          {
+            "selected": true,
+            "text": "1m",
+            "value": "1m"
+          },
+          {
+            "selected": false,
+            "text": "5m",
+            "value": "5m"
+          },
+          {
+            "selected": false,
+            "text": "10m",
+            "value": "10m"
+          },
+          {
+            "selected": false,
+            "text": "30m",
+            "value": "30m"
+          },
+          {
+            "selected": false,
+            "text": "1h",
+            "value": "1h"
+          },
+          {
+            "selected": false,
+            "text": "6h",
+            "value": "6h"
+          },
+          {
+            "selected": false,
+            "text": "12h",
+            "value": "12h"
+          },
+          {
+            "selected": false,
+            "text": "1d",
+            "value": "1d"
+          },
+          {
+            "selected": false,
+            "text": "7d",
+            "value": "7d"
+          },
+          {
+            "selected": false,
+            "text": "14d",
+            "value": "14d"
+          },
+          {
+            "selected": false,
+            "text": "30d",
+            "value": "30d"
+          }
+        ],
+        "query": "1s,10s,30s,1m,5m,10m,30m,1h,6h,12h,1d,7d,14d,30d",
+        "refresh": 2,
+        "type": "interval"
+      }
+    ]
+  },
+  "time": {
+    "from": "now-1h",
+    "to": "now"
+  },
+  "timepicker": {
+    "refresh_intervals": [
+      "5s",
+      "10s",
+      "30s",
+      "1m",
+      "5m",
+      "15m",
+      "30m",
+      "1h",
+      "2h",
+      "1d"
+    ],
+    "time_options": [
+      "5m",
+      "15m",
+      "1h",
+      "6h",
+      "12h",
+      "24h",
+      "2d",
+      "7d",
+      "30d"
+    ]
+  },
+  "timezone": "",
+  "title": "Grafana Fake Data Gen - MySQL",
+  "uid": "DGsCac3kz",
+  "version": 6
+}

+ 547 - 0
docker/blocks/postgres/dashboard.json

@@ -0,0 +1,547 @@
+{
+  "__inputs": [
+    {
+      "name": "DS_POSTGRESQL",
+      "label": "PostgreSQL",
+      "description": "",
+      "type": "datasource",
+      "pluginId": "postgres",
+      "pluginName": "PostgreSQL"
+    }
+  ],
+  "__requires": [
+    {
+      "type": "grafana",
+      "id": "grafana",
+      "name": "Grafana",
+      "version": "5.0.0"
+    },
+    {
+      "type": "panel",
+      "id": "graph",
+      "name": "Graph",
+      "version": ""
+    },
+    {
+      "type": "datasource",
+      "id": "postgres",
+      "name": "PostgreSQL",
+      "version": "1.0.0"
+    },
+    {
+      "type": "panel",
+      "id": "table",
+      "name": "Table",
+      "version": ""
+    }
+  ],
+  "annotations": {
+    "list": [
+      {
+        "builtIn": 1,
+        "datasource": "-- Grafana --",
+        "enable": true,
+        "hide": true,
+        "iconColor": "rgba(0, 211, 255, 1)",
+        "name": "Annotations & Alerts",
+        "type": "dashboard"
+      }
+    ]
+  },
+  "description": "A dashboard visualizing data generated from grafana/fake-data-gen",
+  "editable": true,
+  "gnetId": null,
+  "graphTooltip": 0,
+  "id": null,
+  "iteration": 1518601837383,
+  "links": [],
+  "panels": [
+    {
+      "aliasColors": {
+        "total avg": "#6ed0e0"
+      },
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_POSTGRESQL}",
+      "fill": 2,
+      "gridPos": {
+        "h": 9,
+        "w": 12,
+        "x": 0,
+        "y": 0
+      },
+      "id": 2,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [
+        {
+          "alias": "total avg",
+          "fill": 0,
+          "pointradius": 3,
+          "points": true
+        }
+      ],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "hide": false,
+          "rawSql": "SELECT\n  $__timeGroup(\"createdAt\",'$summarize'),\n  avg(value) as \"value\",\n  hostname as \"metric\"\nFROM \n  grafana_metric\nWHERE\n  $__timeFilter(\"createdAt\") AND\n  measurement = 'logins.count' AND\n  hostname IN($host)\nGROUP BY time, metric\nORDER BY time",
+          "refId": "A",
+          "target": ""
+        },
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT\n  $__timeGroup(\"createdAt\",'$summarize'),\n  min(value) as \"value\",\n  'total avg' as \"metric\"\nFROM \n  grafana_metric\nWHERE\n  $__timeFilter(\"createdAt\") AND\n  measurement = 'logins.count'\nGROUP BY time",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Average logins / $summarize",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_POSTGRESQL}",
+      "fill": 2,
+      "gridPos": {
+        "h": 18,
+        "w": 12,
+        "x": 12,
+        "y": 0
+      },
+      "id": 4,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT\n  $__timeGroup(\"createdAt\",'$summarize'),\n  avg(value) as \"value\",\n  'started' as \"metric\"\nFROM \n  grafana_metric\nWHERE\n  $__timeFilter(\"createdAt\") AND\n  measurement = 'payment.started'\nGROUP BY time, metric\nORDER BY time",
+          "refId": "A",
+          "target": ""
+        },
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT\n  $__timeGroup(\"createdAt\",'$summarize'),\n  avg(value) as \"value\",\n  'ended' as \"metric\"\nFROM \n  grafana_metric\nWHERE\n  $__timeFilter(\"createdAt\") AND\n  measurement = 'payment.ended'\nGROUP BY time, metric\nORDER BY time",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Average payments started/ended / $summarize",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_POSTGRESQL}",
+      "fill": 2,
+      "gridPos": {
+        "h": 9,
+        "w": 12,
+        "x": 0,
+        "y": 9
+      },
+      "id": 3,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT\n  $__timeGroup(\"createdAt\",'$summarize'),\n  max(value) as \"value\",\n  hostname as \"metric\"\nFROM \n  grafana_metric\nWHERE\n  $__timeFilter(\"createdAt\") AND\n  measurement = 'cpu' AND\n  hostname IN($host)\nGROUP BY time, metric\nORDER BY time",
+          "refId": "A",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Max CPU / $summarize",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "percent",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "columns": [],
+      "datasource": "${DS_POSTGRESQL}",
+      "fontSize": "100%",
+      "gridPos": {
+        "h": 9,
+        "w": 24,
+        "x": 0,
+        "y": 18
+      },
+      "id": 6,
+      "links": [],
+      "pageSize": null,
+      "scroll": true,
+      "showHeader": true,
+      "sort": {
+        "col": 0,
+        "desc": true
+      },
+      "styles": [
+        {
+          "alias": "Time",
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "link": false,
+          "pattern": "Time",
+          "type": "date"
+        },
+        {
+          "alias": "",
+          "colorMode": null,
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "decimals": 2,
+          "pattern": "/.*/",
+          "thresholds": [],
+          "type": "number",
+          "unit": "short"
+        }
+      ],
+      "targets": [
+        {
+          "alias": "",
+          "format": "table",
+          "rawSql": "SELECT \"createdAt\" as \"Time\", source, datacenter, hostname, value FROM grafana_metric WHERE hostname in($host)",
+          "refId": "A",
+          "target": ""
+        }
+      ],
+      "title": "Values",
+      "transform": "table",
+      "type": "table"
+    }
+  ],
+  "schemaVersion": 16,
+  "style": "dark",
+  "tags": [
+    "fake-data-gen",
+    "postgres"
+  ],
+  "templating": {
+    "list": [
+      {
+        "allValue": null,
+        "current": {},
+        "datasource": "${DS_POSTGRESQL}",
+        "hide": 0,
+        "includeAll": false,
+        "label": "Datacenter",
+        "multi": false,
+        "name": "datacenter",
+        "options": [],
+        "query": "SELECT DISTINCT datacenter FROM grafana_metric",
+        "refresh": 1,
+        "regex": "",
+        "sort": 1,
+        "tagValuesQuery": "",
+        "tags": [],
+        "tagsQuery": "",
+        "type": "query",
+        "useTags": false
+      },
+      {
+        "allValue": null,
+        "current": {},
+        "datasource": "${DS_POSTGRESQL}",
+        "hide": 0,
+        "includeAll": true,
+        "label": "Hostname",
+        "multi": true,
+        "name": "host",
+        "options": [],
+        "query": "SELECT DISTINCT hostname FROM grafana_metric WHERE datacenter='$datacenter'",
+        "refresh": 1,
+        "regex": "",
+        "sort": 1,
+        "tagValuesQuery": "",
+        "tags": [],
+        "tagsQuery": "",
+        "type": "query",
+        "useTags": false
+      },
+      {
+        "auto": false,
+        "auto_count": 5,
+        "auto_min": "10s",
+        "current": {
+          "text": "1m",
+          "value": "1m"
+        },
+        "hide": 0,
+        "label": "Summarize",
+        "name": "summarize",
+        "options": [
+          {
+            "selected": false,
+            "text": "1s",
+            "value": "1s"
+          },
+          {
+            "selected": false,
+            "text": "10s",
+            "value": "10s"
+          },
+          {
+            "selected": false,
+            "text": "30s",
+            "value": "30s"
+          },
+          {
+            "selected": true,
+            "text": "1m",
+            "value": "1m"
+          },
+          {
+            "selected": false,
+            "text": "5m",
+            "value": "5m"
+          },
+          {
+            "selected": false,
+            "text": "10m",
+            "value": "10m"
+          },
+          {
+            "selected": false,
+            "text": "30m",
+            "value": "30m"
+          },
+          {
+            "selected": false,
+            "text": "1h",
+            "value": "1h"
+          },
+          {
+            "selected": false,
+            "text": "6h",
+            "value": "6h"
+          },
+          {
+            "selected": false,
+            "text": "12h",
+            "value": "12h"
+          },
+          {
+            "selected": false,
+            "text": "1d",
+            "value": "1d"
+          },
+          {
+            "selected": false,
+            "text": "7d",
+            "value": "7d"
+          },
+          {
+            "selected": false,
+            "text": "14d",
+            "value": "14d"
+          },
+          {
+            "selected": false,
+            "text": "30d",
+            "value": "30d"
+          }
+        ],
+        "query": "1s,10s,30s,1m,5m,10m,30m,1h,6h,12h,1d,7d,14d,30d",
+        "refresh": 2,
+        "type": "interval"
+      }
+    ]
+  },
+  "time": {
+    "from": "now-1h",
+    "to": "now"
+  },
+  "timepicker": {
+    "refresh_intervals": [
+      "5s",
+      "10s",
+      "30s",
+      "1m",
+      "5m",
+      "15m",
+      "30m",
+      "1h",
+      "2h",
+      "1d"
+    ],
+    "time_options": [
+      "5m",
+      "15m",
+      "1h",
+      "6h",
+      "12h",
+      "24h",
+      "2d",
+      "7d",
+      "30d"
+    ]
+  },
+  "timezone": "",
+  "title": "Grafana Fake Data Gen - PostgreSQL",
+  "uid": "JYola5qzz",
+  "version": 1
+}

+ 30 - 15
docs/sources/administration/provisioning.md

@@ -81,13 +81,16 @@ If you are running multiple instances of Grafana you might run into problems if
 
 
 ### Example datasource config file
 ### Example datasource config file
 ```yaml
 ```yaml
+# config file version
+apiVersion: 1
+
 # list of datasources that should be deleted from the database
 # list of datasources that should be deleted from the database
-delete_datasources:
+deleteDatasources:
   - name: Graphite
   - name: Graphite
-    org_id: 1
+    orgId: 1
 
 
 # list of datasources to insert/update depending
 # list of datasources to insert/update depending
-# whats available in the datbase
+# whats available in the database
 datasources:
 datasources:
   # <string, required> name of the datasource. Required
   # <string, required> name of the datasource. Required
 - name: Graphite
 - name: Graphite
@@ -95,8 +98,8 @@ datasources:
   type: graphite
   type: graphite
   # <string, required> access mode. direct or proxy. Required
   # <string, required> access mode. direct or proxy. Required
   access: proxy
   access: proxy
-  # <int> org id. will default to org_id 1 if not specified
-  org_id: 1
+  # <int> org id. will default to orgId 1 if not specified
+  orgId: 1
   # <string> url
   # <string> url
   url: http://localhost:8080
   url: http://localhost:8080
   # <string> database password, if used
   # <string> database password, if used
@@ -106,22 +109,22 @@ datasources:
   # <string> database name, if used
   # <string> database name, if used
   database:
   database:
   # <bool> enable/disable basic auth
   # <bool> enable/disable basic auth
-  basic_auth:
+  basicAuth:
   # <string> basic auth username
   # <string> basic auth username
-  basic_auth_user:
+  basicAuthUser:
   # <string> basic auth password
   # <string> basic auth password
-  basic_auth_password:
+  basicAuthPassword:
   # <bool> enable/disable with credentials headers
   # <bool> enable/disable with credentials headers
-  with_credentials:
+  withCredentials:
   # <bool> mark as default datasource. Max one per org
   # <bool> mark as default datasource. Max one per org
-  is_default:
+  isDefault:
   # <map> fields that will be converted to json and stored in json_data
   # <map> fields that will be converted to json and stored in json_data
-  json_data:
+  jsonData:
      graphiteVersion: "1.1"
      graphiteVersion: "1.1"
      tlsAuth: true
      tlsAuth: true
      tlsAuthWithCACert: true
      tlsAuthWithCACert: true
   # <string> json object of data that will be encrypted.
   # <string> json object of data that will be encrypted.
-  secure_json_data:
+  secureJsonData:
     tlsCACert: "..."
     tlsCACert: "..."
     tlsClientCert: "..."
     tlsClientCert: "..."
     tlsClientKey: "..."
     tlsClientKey: "..."
@@ -155,7 +158,7 @@ Since not all datasources have the same configuration settings we only have the
 
 
 #### Secure Json data
 #### Secure Json data
 
 
-{"authType":"keys","defaultRegion":"us-west-2","timeField":"@timestamp"}
+`{"authType":"keys","defaultRegion":"us-west-2","timeField":"@timestamp"}`
 
 
 Secure json data is a map of settings that will be encrypted with [secret key](/installation/configuration/#secret-key) from the Grafana config. The purpose of this is only to hide content from the users of the application. This should be used for storing TLS Cert and password that Grafana will append to the request on the server side. All of these settings are optional.
 Secure json data is a map of settings that will be encrypted with [secret key](/installation/configuration/#secret-key) from the Grafana config. The purpose of this is only to hide content from the users of the application. This should be used for storing TLS Cert and password that Grafana will append to the request on the server side. All of these settings are optional.
 
 
@@ -169,15 +172,20 @@ Secure json data is a map of settings that will be encrypted with [secret key](/
 
 
 ### Dashboards
 ### Dashboards
 
 
-It's possible to manage dashboards in Grafana by adding one or more yaml config files in the [`provisioning/dashboards`](/installation/configuration/#provisioning) directory. Each config file can contain a list of `dashboards providers` that will load dashboards into Grafana. Currently we only support reading dashboards from file but we will add more providers in the future.
+It's possible to manage dashboards in Grafana by adding one or more yaml config files in the [`provisioning/dashboards`](/installation/configuration/#provisioning) directory. Each config file can contain a list of `dashboards providers` that will load dashboards into Grafana from the local filesystem.
 
 
 The dashboard provider config file looks somewhat like this:
 The dashboard provider config file looks somewhat like this:
 
 
 ```yaml
 ```yaml
+apiVersion: 1
+
+providers:
 - name: 'default'
 - name: 'default'
-  org_id: 1
+  orgId: 1
   folder: ''
   folder: ''
   type: file
   type: file
+  disableDeletion: false
+  editable: false
   options:
   options:
     path: /var/lib/grafana/dashboards
     path: /var/lib/grafana/dashboards
 ```
 ```
@@ -187,3 +195,10 @@ When Grafana starts, it will update/insert all dashboards available in the confi
 ### Reuseable dashboard urls
 ### Reuseable dashboard urls
 
 
 If the dashboard in the json file contains an [uid](/reference/dashboard/#json-fields), Grafana will force insert/update on that uid. This allows you to migrate dashboards betweens Grafana instances and provisioning Grafana from configuration without breaking the urls given since the new dashboard url uses the uid as identifer.
 If the dashboard in the json file contains an [uid](/reference/dashboard/#json-fields), Grafana will force insert/update on that uid. This allows you to migrate dashboards betweens Grafana instances and provisioning Grafana from configuration without breaking the urls given since the new dashboard url uses the uid as identifer.
+When Grafana starts, it will update/insert all dashboards available in the configured folders. If you modify the file, the dashboard will also be updated.
+By default Grafana will delete dashboards in the database if the file is removed. You can disable this behavior using the `disableDeletion` setting.
+
+> **Note.** Provisioning allows you to overwrite existing dashboards
+> which leads to problems if you re-use settings that are supposed to be unique.
+> Be careful not to re-use the same `title` multiple times within a folder
+> or `uid` within the same installation as this will cause weird behaviours.

+ 1 - 1
docs/sources/features/datasources/cloudwatch.md

@@ -13,7 +13,7 @@ weight = 10
 
 
 # Using AWS CloudWatch in Grafana
 # Using AWS CloudWatch in Grafana
 
 
-Grafana ships with built in support for CloudWatch. You just have to add it as a data source and you will be ready to build dashboards for you CloudWatch metrics.
+Grafana ships with built in support for CloudWatch. You just have to add it as a data source and you will be ready to build dashboards for your CloudWatch metrics.
 
 
 ## Adding the data source to Grafana
 ## Adding the data source to Grafana
 
 

+ 1 - 1
docs/sources/guides/whats-new-in-v5.md

@@ -12,7 +12,7 @@ weight = -6
 
 
 # What's New in Grafana v5.0
 # What's New in Grafana v5.0
 
 
-> Out in beta: [Download now!](https://grafana.com/grafana/download/5.0.0-beta1)
+> Out in beta: [Download now!](https://grafana.com/grafana/download/beta)
 
 
 This is the most substantial update that Grafana has ever seen. This article will detail the major new features and enhancements.
 This is the most substantial update that Grafana has ever seen. This article will detail the major new features and enhancements.
 
 

+ 1 - 1
docs/sources/http_api/dashboard.md

@@ -110,7 +110,7 @@ Content-Length: 97
 }
 }
 ```
 ```
 
 
-In in case of title already exists the `status` property will be `name-exists`.
+In case of title already exists the `status` property will be `name-exists`.
 
 
 ## Get dashboard by uid
 ## Get dashboard by uid
 
 

+ 3 - 3
docs/sources/installation/debian.md

@@ -16,7 +16,7 @@ weight = 1
 Description | Download
 Description | Download
 ------------ | -------------
 ------------ | -------------
 Stable for Debian-based Linux | [grafana_4.6.3_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.6.3_amd64.deb)
 Stable for Debian-based Linux | [grafana_4.6.3_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.6.3_amd64.deb)
-Beta for Debian-based Linux | [grafana_5.0.0-beta1_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.0-beta1_amd64.deb)
+Beta for Debian-based Linux | [grafana_5.0.0-beta4_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.0-beta4_amd64.deb)
 
 
 Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
 Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
 installation.
 installation.
@@ -33,9 +33,9 @@ sudo dpkg -i grafana_4.6.3_amd64.deb
 ## Install Latest Beta
 ## Install Latest Beta
 
 
 ```bash
 ```bash
-wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.0-beta1_amd64.deb
+wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.0-beta4_amd64.deb
 sudo apt-get install -y adduser libfontconfig
 sudo apt-get install -y adduser libfontconfig
-sudo dpkg -i grafana_5.0.0-beta1_amd64.deb
+sudo dpkg -i grafana_5.0.0-beta4_amd64.deb
 
 
 ```
 ```
 ## APT Repository
 ## APT Repository

+ 1 - 1
docs/sources/installation/ldap.md

@@ -43,7 +43,7 @@ ssl_skip_verify = false
 # Search user bind dn
 # Search user bind dn
 bind_dn = "cn=admin,dc=grafana,dc=org"
 bind_dn = "cn=admin,dc=grafana,dc=org"
 # Search user bind password
 # Search user bind password
-# If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;"""
+# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;"""
 bind_password = 'grafana'
 bind_password = 'grafana'
 
 
 # User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"
 # User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"

+ 2 - 2
docs/sources/installation/rpm.md

@@ -16,7 +16,7 @@ weight = 2
 Description | Download
 Description | Download
 ------------ | -------------
 ------------ | -------------
 Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [4.6.3 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.3-1.x86_64.rpm)
 Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [4.6.3 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.3-1.x86_64.rpm)
-Latest Beta for CentOS / Fedora / OpenSuse / Redhat Linux | [5.0.0-beta1 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta1.x86_64.rpm)
+Latest Beta for CentOS / Fedora / OpenSuse / Redhat Linux | [5.0.0-beta4 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta4.x86_64.rpm)
 
 
 Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
 Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
 installation.
 installation.
@@ -32,7 +32,7 @@ $ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/g
 ## Install Beta
 ## Install Beta
 
 
 ```bash
 ```bash
-$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta1.x86_64.rpm
+$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta4.x86_64.rpm
 ```
 ```
 
 
 Or install manually using `rpm`.
 Or install manually using `rpm`.

+ 4 - 1
docs/sources/installation/upgrading.md

@@ -105,4 +105,7 @@ We are not aware of any issues upgrading directly from 2.x to 4.x but to be on t
 ## Upgrading to v5.0
 ## Upgrading to v5.0
 
 
 The dashboard grid layout engine has changed. All dashboards will be automatically upgraded to new
 The dashboard grid layout engine has changed. All dashboards will be automatically upgraded to new
-positioning system when you load them in v5. Dashboards saved in v5 will not work in older versions of Grafana.
+positioning system when you load them in v5. Dashboards saved in v5 will not work in older versions of Grafana. Some
+external panel plugins might need to be updated to work properly.
+
+For more details on the new panel positioning system, [click here]({{< relref "reference/dashboard.md#panel-size-position" >}})

+ 1 - 1
docs/sources/installation/windows.md

@@ -14,7 +14,7 @@ weight = 3
 Description | Download
 Description | Download
 ------------ | -------------
 ------------ | -------------
 Latest stable package for Windows | [grafana.4.6.3.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.3.windows-x64.zip)
 Latest stable package for Windows | [grafana.4.6.3.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.3.windows-x64.zip)
-Latest beta package for Windows | [grafana.5.0.0-beta1.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta1.windows-x64.zip)
+Latest beta package for Windows | [grafana.5.0.0-beta4.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta4.windows-x64.zip)
 
 
 Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
 Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
 installation.
 installation.

+ 29 - 156
docs/sources/reference/dashboard.md

@@ -50,7 +50,7 @@ When a user creates a new dashboard, a new dashboard JSON object is initialized
   "annotations": {
   "annotations": {
     "list": []
     "list": []
   },
   },
-  "schemaVersion": 7,
+  "schemaVersion": 16,
   "version": 0,
   "version": 0,
   "links": []
   "links": []
 }
 }
@@ -60,13 +60,12 @@ Each field in the dashboard JSON is explained below with its usage:
 | Name | Usage |
 | Name | Usage |
 | ---- | ----- |
 | ---- | ----- |
 | **id** | unique numeric identifier for the dashboard. (generated by the db) |
 | **id** | unique numeric identifier for the dashboard. (generated by the db) |
-| **uid** | unique dashboard identifier that can be generated by anyone. string (8-40) | 
+| **uid** | unique dashboard identifier that can be generated by anyone. string (8-40) |
 | **title** | current title of dashboard |
 | **title** | current title of dashboard |
 | **tags** | tags associated with dashboard, an array of strings |
 | **tags** | tags associated with dashboard, an array of strings |
 | **style** | theme of dashboard, i.e. `dark` or `light` |
 | **style** | theme of dashboard, i.e. `dark` or `light` |
 | **timezone** | timezone of dashboard, i.e. `utc` or `browser` |
 | **timezone** | timezone of dashboard, i.e. `utc` or `browser` |
 | **editable** | whether a dashboard is editable or not |
 | **editable** | whether a dashboard is editable or not |
-| **hideControls** | whether row controls on the left in green are hidden or not |
 | **graphTooltip** | 0 for no shared crosshair or tooltip (default), 1 for shared crosshair, 2 for shared crosshair AND shared tooltip |
 | **graphTooltip** | 0 for no shared crosshair or tooltip (default), 1 for shared crosshair, 2 for shared crosshair AND shared tooltip |
 | **time** | time range for dashboard, i.e. last 6 hours, last 7 days, etc |
 | **time** | time range for dashboard, i.e. last 6 hours, last 7 days, etc |
 | **timepicker** | timepicker metadata, see [timepicker section](#timepicker) for details |
 | **timepicker** | timepicker metadata, see [timepicker section](#timepicker) for details |
@@ -74,166 +73,40 @@ Each field in the dashboard JSON is explained below with its usage:
 | **annotations** | annotations metadata, see [annotations section](#annotations) for details |
 | **annotations** | annotations metadata, see [annotations section](#annotations) for details |
 | **schemaVersion** | version of the JSON schema (integer), incremented each time a Grafana update brings changes to the said schema |
 | **schemaVersion** | version of the JSON schema (integer), incremented each time a Grafana update brings changes to the said schema |
 | **version** | version of the dashboard (integer), incremented each time the dashboard is updated |
 | **version** | version of the dashboard (integer), incremented each time the dashboard is updated |
-| **links** | TODO |
+| **panels** | panels array, see below for detail. |
 
 
-#### panels
+## Panels
 
 
-Panels are the building blocks a dashboard. It consists of datasource queries, type of graphs, aliases, etc. Panel JSON consists of an array of JSON objects, each representing a different panel in a row. Most of the fields are common for all panels but some fields depends on the panel type. Following is an example of panel JSON representing a `graph` panel type:
+Panels are the building blocks a dashboard. It consists of datasource queries, type of graphs, aliases, etc. Panel JSON consists of an array of JSON objects, each representing a different panel. Most of the fields are common for all panels but some fields depends on the panel type. Following is an example of panel JSON of a text panel.
 
 
 ```json
 ```json
 "panels": [
 "panels": [
-        {
-          "aliasColors": {},
-          "bars": false,
-          "datasource": null,
-          "editable": true,
-          "error": false,
-          "fill": 0,
-          "grid": {
-            "leftLogBase": 1,
-            "leftMax": null,
-            "leftMin": null,
-            "rightLogBase": 1,
-            "rightMax": null,
-            "rightMin": null,
-            "threshold1": null,
-            "threshold1Color": "rgba(216, 200, 27, 0.27)",
-            "threshold2": null,
-            "threshold2Color": "rgba(234, 112, 112, 0.22)"
-          },
-          "id": 1,
-          "legend": {
-            "avg": false,
-            "current": false,
-            "max": false,
-            "min": false,
-            "show": true,
-            "total": false,
-            "values": false
-          },
-          "lines": true,
-          "linewidth": 1,
-          "links": [],
-          "nullPointMode": "connected",
-          "percentage": false,
-          "pointradius": 5,
-          "points": false,
-          "renderer": "flot",
-          "seriesOverrides": [],
-          "span": 4,
-          "stack": false,
-          "steppedLine": false,
-          "targets": [
-            {
-              "aggregator": "max",
-              "alias": "$tag_instance_id",
-              "currentTagKey": "",
-              "currentTagValue": "",
-              "downsampleAggregator": "avg",
-              "downsampleInterval": "",
-              "errors": {},
-              "metric": "memory.percent-used",
-              "refId": "A",
-              "shouldComputeRate": false,
-              "tags": {
-                "app": "$app",
-                "env": "stage",
-                "instance_id": "*"
-              }
-            }
-          ],
-          "timeFrom": null,
-          "timeShift": null,
-          "title": "Memory Utilization",
-          "tooltip": {
-            "shared": true,
-            "value_type": "cumulative"
-          },
-          "type": "graph",
-          "x-axis": true,
-          "y-axis": true,
-          "y_formats": [
-            "percent",
-            "short"
-          ]
-        },
-        {
-          "aliasColors": {},
-          "bars": false,
-          "datasource": null,
-          "editable": true,
-          "error": false,
-          "fill": 0,
-          "grid": {
-            "leftLogBase": 1,
-            "leftMax": null,
-            "leftMin": null,
-            "rightLogBase": 1,
-            "rightMax": null,
-            "rightMin": null,
-            "threshold1": null,
-            "threshold1Color": "rgba(216, 200, 27, 0.27)",
-            "threshold2": null,
-            "threshold2Color": "rgba(234, 112, 112, 0.22)"
-          },
-          "id": 2,
-          "legend": {
-            "avg": false,
-            "current": false,
-            "max": false,
-            "min": false,
-            "show": true,
-            "total": false,
-            "values": false
-          },
-          "lines": true,
-          "linewidth": 1,
-          "links": [],
-          "nullPointMode": "connected",
-          "percentage": false,
-          "pointradius": 5,
-          "points": false,
-          "renderer": "flot",
-          "seriesOverrides": [],
-          "span": 4,
-          "stack": false,
-          "steppedLine": false,
-          "targets": [
-            {
-              "aggregator": "avg",
-              "alias": "$tag_instance_id",
-              "currentTagKey": "",
-              "currentTagValue": "",
-              "downsampleAggregator": "avg",
-              "downsampleInterval": "",
-              "errors": {},
-              "metric": "memory.percent-cached",
-              "refId": "A",
-              "shouldComputeRate": false,
-              "tags": {
-                "app": "$app",
-                "env": "prod",
-                "instance_id": "*"
-              }
-            }
-          ],
-          "timeFrom": null,
-          "timeShift": null,
-          "title": "Memory Cached",
-          "tooltip": {
-            "shared": true,
-            "value_type": "cumulative"
-          },
-          "type": "graph",
-          "x-axis": true,
-          "y-axis": true,
-          "y_formats": [
-            "short",
-            "short"
-          ]
-        },
+  {
+    "type": "text",
+    "title": "Panel Title",
+    "gridPos": {
+      "x": 0,
+      "y": 0,
+      "w": 12,
+      "h": 9
+    },
+    "id": 4,
+    "mode": "markdown",
+    "content": "# title"
+  }
 ```
 ```
 
 
+### Panel size & position
+
+The gridPos property describes the panel size and position in grid coordinates.
+
+- `w` 1-24  (the width of the dashboard is divided into 24 columns)
+- `h` In grid height units, each represents 30 pixels.
+- `x` The x position, in same unit as `w`.
+- `y` The y position, in same unit as `h`.
+
+The grid has a negative gravity that moves panels up if there i empty space above a panel.
+
 ### timepicker
 ### timepicker
 
 
 ```json
 ```json

+ 4 - 1
package.json

@@ -4,7 +4,7 @@
     "company": "Grafana Labs"
     "company": "Grafana Labs"
   },
   },
   "name": "grafana",
   "name": "grafana",
-  "version": "5.0.0-beta1",
+  "version": "5.0.0-beta4",
   "repository": {
   "repository": {
     "type": "git",
     "type": "git",
     "url": "http://github.com/grafana/grafana.git"
     "url": "http://github.com/grafana/grafana.git"
@@ -19,6 +19,7 @@
     "angular-mocks": "^1.6.6",
     "angular-mocks": "^1.6.6",
     "autoprefixer": "^6.4.0",
     "autoprefixer": "^6.4.0",
     "awesome-typescript-loader": "^3.2.3",
     "awesome-typescript-loader": "^3.2.3",
+    "axios": "^0.17.1",
     "babel-core": "^6.26.0",
     "babel-core": "^6.26.0",
     "babel-loader": "^7.1.2",
     "babel-loader": "^7.1.2",
     "babel-preset-es2015": "^6.24.1",
     "babel-preset-es2015": "^6.24.1",
@@ -105,6 +106,7 @@
     "lint": "tslint -c tslint.json --project tsconfig.json --type-check",
     "lint": "tslint -c tslint.json --project tsconfig.json --type-check",
     "karma": "node ./node_modules/grunt-cli/bin/grunt karma:dev",
     "karma": "node ./node_modules/grunt-cli/bin/grunt karma:dev",
     "jest": "node ./node_modules/jest-cli/bin/jest.js --notify --watch",
     "jest": "node ./node_modules/jest-cli/bin/jest.js --notify --watch",
+    "api-tests": "node ./node_modules/jest-cli/bin/jest.js --notify --watch --config=tests/api/jest.js",
     "precommit": "lint-staged && node ./node_modules/grunt-cli/bin/grunt precommit"
     "precommit": "lint-staged && node ./node_modules/grunt-cli/bin/grunt precommit"
   },
   },
   "lint-staged": {
   "lint-staged": {
@@ -148,6 +150,7 @@
     "mobx-state-tree": "^1.3.1",
     "mobx-state-tree": "^1.3.1",
     "moment": "^2.18.1",
     "moment": "^2.18.1",
     "mousetrap": "^1.6.0",
     "mousetrap": "^1.6.0",
+    "mousetrap-global-bind": "^1.1.0",
     "perfect-scrollbar": "^1.2.0",
     "perfect-scrollbar": "^1.2.0",
     "prop-types": "^15.6.0",
     "prop-types": "^15.6.0",
     "react": "^16.2.0",
     "react": "^16.2.0",

+ 2 - 2
packaging/publish/publish_testing.sh

@@ -1,6 +1,6 @@
 #! /usr/bin/env bash
 #! /usr/bin/env bash
-deb_ver=5.0.0-beta1
-rpm_ver=5.0.0-beta1
+deb_ver=5.0.0-beta4
+rpm_ver=5.0.0-beta4
 
 
 wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_${deb_ver}_amd64.deb
 wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_${deb_ver}_amd64.deb
 
 

+ 5 - 67
pkg/api/alerting.go

@@ -52,6 +52,7 @@ func GetAlerts(c *middleware.Context) Response {
 		DashboardId: c.QueryInt64("dashboardId"),
 		DashboardId: c.QueryInt64("dashboardId"),
 		PanelId:     c.QueryInt64("panelId"),
 		PanelId:     c.QueryInt64("panelId"),
 		Limit:       c.QueryInt64("limit"),
 		Limit:       c.QueryInt64("limit"),
+		User:        c.SignedInUser,
 	}
 	}
 
 
 	states := c.QueryStrings("state")
 	states := c.QueryStrings("state")
@@ -63,74 +64,11 @@ func GetAlerts(c *middleware.Context) Response {
 		return ApiError(500, "List alerts failed", err)
 		return ApiError(500, "List alerts failed", err)
 	}
 	}
 
 
-	alertDTOs, resp := transformToDTOs(query.Result, c)
-	if resp != nil {
-		return resp
+	for _, alert := range query.Result {
+		alert.Url = models.GetDashboardUrl(alert.DashboardUid, alert.DashboardSlug)
 	}
 	}
 
 
-	return Json(200, alertDTOs)
-}
-
-func transformToDTOs(alerts []*models.Alert, c *middleware.Context) ([]*dtos.AlertRule, Response) {
-	if len(alerts) == 0 {
-		return []*dtos.AlertRule{}, nil
-	}
-
-	dashboardIds := make([]int64, 0)
-	alertDTOs := make([]*dtos.AlertRule, 0)
-	for _, alert := range alerts {
-		dashboardIds = append(dashboardIds, alert.DashboardId)
-		alertDTOs = append(alertDTOs, &dtos.AlertRule{
-			Id:             alert.Id,
-			DashboardId:    alert.DashboardId,
-			PanelId:        alert.PanelId,
-			Name:           alert.Name,
-			Message:        alert.Message,
-			State:          alert.State,
-			NewStateDate:   alert.NewStateDate,
-			ExecutionError: alert.ExecutionError,
-			EvalData:       alert.EvalData,
-		})
-	}
-
-	dashboardsQuery := models.GetDashboardsQuery{
-		DashboardIds: dashboardIds,
-	}
-
-	if err := bus.Dispatch(&dashboardsQuery); err != nil {
-		return nil, ApiError(500, "List alerts failed", err)
-	}
-
-	//TODO: should be possible to speed this up with lookup table
-	for _, alert := range alertDTOs {
-		for _, dash := range dashboardsQuery.Result {
-			if alert.DashboardId == dash.Id {
-				alert.Url = dash.GenerateUrl()
-				break
-			}
-		}
-	}
-
-	permissionsQuery := models.GetDashboardPermissionsForUserQuery{
-		DashboardIds: dashboardIds,
-		OrgId:        c.OrgId,
-		UserId:       c.SignedInUser.UserId,
-		OrgRole:      c.SignedInUser.OrgRole,
-	}
-
-	if err := bus.Dispatch(&permissionsQuery); err != nil {
-		return nil, ApiError(500, "List alerts failed", err)
-	}
-
-	for _, alert := range alertDTOs {
-		for _, perm := range permissionsQuery.Result {
-			if alert.DashboardId == perm.DashboardId {
-				alert.CanEdit = perm.Permission > 1
-			}
-		}
-	}
-
-	return alertDTOs, nil
+	return Json(200, query.Result)
 }
 }
 
 
 // POST /api/alerts/test
 // POST /api/alerts/test
@@ -288,7 +226,7 @@ func PauseAlert(c *middleware.Context, dto dtos.PauseAlertCommand) Response {
 		return ApiError(500, "Get Alert failed", err)
 		return ApiError(500, "Get Alert failed", err)
 	}
 	}
 
 
-	guardian := guardian.NewDashboardGuardian(query.Result.DashboardId, c.OrgId, c.SignedInUser)
+	guardian := guardian.New(query.Result.DashboardId, c.OrgId, c.SignedInUser)
 	if canEdit, err := guardian.CanEdit(); err != nil || !canEdit {
 	if canEdit, err := guardian.CanEdit(); err != nil || !canEdit {
 		if err != nil {
 		if err != nil {
 			return ApiError(500, "Error while checking permissions for Alert", err)
 			return ApiError(500, "Error while checking permissions for Alert", err)

+ 1 - 1
pkg/api/annotations.go

@@ -278,7 +278,7 @@ func canSaveByDashboardId(c *middleware.Context, dashboardId int64) (bool, error
 	}
 	}
 
 
 	if dashboardId > 0 {
 	if dashboardId > 0 {
-		guardian := guardian.NewDashboardGuardian(dashboardId, c.OrgId, c.SignedInUser)
+		guardian := guardian.New(dashboardId, c.OrgId, c.SignedInUser)
 		if canEdit, err := guardian.CanEdit(); err != nil || !canEdit {
 		if canEdit, err := guardian.CanEdit(); err != nil || !canEdit {
 			return false, err
 			return false, err
 		}
 		}

+ 0 - 3
pkg/api/api.go

@@ -261,8 +261,6 @@ func (hs *HttpServer) registerRoutes() {
 			dashboardRoute.Get("/tags", GetDashboardTags)
 			dashboardRoute.Get("/tags", GetDashboardTags)
 			dashboardRoute.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard))
 			dashboardRoute.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard))
 
 
-			dashboardRoute.Get("/folders", wrap(GetFoldersForSignedInUser))
-
 			dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute RouteRegister) {
 			dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute RouteRegister) {
 				dashIdRoute.Get("/versions", wrap(GetDashboardVersions))
 				dashIdRoute.Get("/versions", wrap(GetDashboardVersions))
 				dashIdRoute.Get("/versions/:id", wrap(GetDashboardVersion))
 				dashIdRoute.Get("/versions/:id", wrap(GetDashboardVersion))
@@ -271,7 +269,6 @@ func (hs *HttpServer) registerRoutes() {
 				dashIdRoute.Group("/acl", func(aclRoute RouteRegister) {
 				dashIdRoute.Group("/acl", func(aclRoute RouteRegister) {
 					aclRoute.Get("/", wrap(GetDashboardAclList))
 					aclRoute.Get("/", wrap(GetDashboardAclList))
 					aclRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateDashboardAcl))
 					aclRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateDashboardAcl))
-					aclRoute.Delete("/:aclId", wrap(DeleteDashboardAcl))
 				})
 				})
 			})
 			})
 		})
 		})

+ 21 - 58
pkg/api/dashboard.go

@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"fmt"
 	"os"
 	"os"
 	"path"
 	"path"
-	"strings"
 
 
 	"github.com/grafana/grafana/pkg/services/dashboards"
 	"github.com/grafana/grafana/pkg/services/dashboards"
 
 
@@ -50,7 +49,7 @@ func GetDashboard(c *middleware.Context) Response {
 		return rsp
 		return rsp
 	}
 	}
 
 
-	guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
+	guardian := guardian.New(dash.Id, c.OrgId, c.SignedInUser)
 	if canView, err := guardian.CanView(); err != nil || !canView {
 	if canView, err := guardian.CanView(); err != nil || !canView {
 		return dashboardGuardianResponse(err)
 		return dashboardGuardianResponse(err)
 	}
 	}
@@ -157,7 +156,7 @@ func DeleteDashboard(c *middleware.Context) Response {
 		return rsp
 		return rsp
 	}
 	}
 
 
-	guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
+	guardian := guardian.New(dash.Id, c.OrgId, c.SignedInUser)
 	if canSave, err := guardian.CanSave(); err != nil || !canSave {
 	if canSave, err := guardian.CanSave(); err != nil || !canSave {
 		return dashboardGuardianResponse(err)
 		return dashboardGuardianResponse(err)
 	}
 	}
@@ -177,7 +176,7 @@ func DeleteDashboardByUid(c *middleware.Context) Response {
 		return rsp
 		return rsp
 	}
 	}
 
 
-	guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
+	guardian := guardian.New(dash.Id, c.OrgId, c.SignedInUser)
 	if canSave, err := guardian.CanSave(); err != nil || !canSave {
 	if canSave, err := guardian.CanSave(); err != nil || !canSave {
 		return dashboardGuardianResponse(err)
 		return dashboardGuardianResponse(err)
 	}
 	}
@@ -197,32 +196,7 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
 
 
 	dash := cmd.GetDashboardModel()
 	dash := cmd.GetDashboardModel()
 
 
-	dashId := dash.Id
-
-	// if new dashboard, use parent folder permissions instead
-	if dashId == 0 {
-		dashId = cmd.FolderId
-	}
-
-	guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
-	if canSave, err := guardian.CanSave(); err != nil || !canSave {
-		return dashboardGuardianResponse(err)
-	}
-
-	if dash.IsFolder && dash.FolderId > 0 {
-		return ApiError(400, m.ErrDashboardFolderCannotHaveParent.Error(), nil)
-	}
-
-	// Check if Title is empty
-	if dash.Title == "" {
-		return ApiError(400, m.ErrDashboardTitleEmpty.Error(), nil)
-	}
-
-	if dash.IsFolder && strings.ToLower(dash.Title) == strings.ToLower(m.RootFolderName) {
-		return ApiError(400, "A folder already exists with that name", nil)
-	}
-
-	if dash.Id == 0 {
+	if dash.Id == 0 && dash.Uid == "" {
 		limitReached, err := middleware.QuotaReached(c, "dashboard")
 		limitReached, err := middleware.QuotaReached(c, "dashboard")
 		if err != nil {
 		if err != nil {
 			return ApiError(500, "failed to get quota", err)
 			return ApiError(500, "failed to get quota", err)
@@ -232,31 +206,38 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
 		}
 		}
 	}
 	}
 
 
-	dashItem := &dashboards.SaveDashboardItem{
+	dashItem := &dashboards.SaveDashboardDTO{
 		Dashboard: dash,
 		Dashboard: dash,
 		Message:   cmd.Message,
 		Message:   cmd.Message,
 		OrgId:     c.OrgId,
 		OrgId:     c.OrgId,
-		UserId:    c.UserId,
+		User:      c.SignedInUser,
 		Overwrite: cmd.Overwrite,
 		Overwrite: cmd.Overwrite,
 	}
 	}
 
 
-	dashboard, err := dashboards.GetRepository().SaveDashboard(dashItem)
+	dashboard, err := dashboards.NewService().SaveDashboard(dashItem)
 
 
 	if err == m.ErrDashboardTitleEmpty ||
 	if err == m.ErrDashboardTitleEmpty ||
 		err == m.ErrDashboardWithSameNameAsFolder ||
 		err == m.ErrDashboardWithSameNameAsFolder ||
 		err == m.ErrDashboardFolderWithSameNameAsDashboard ||
 		err == m.ErrDashboardFolderWithSameNameAsDashboard ||
-		err == m.ErrDashboardTypeMismatch {
+		err == m.ErrDashboardTypeMismatch ||
+		err == m.ErrDashboardInvalidUid ||
+		err == m.ErrDashboardUidToLong ||
+		err == m.ErrDashboardWithSameUIDExists ||
+		err == m.ErrFolderNotFound ||
+		err == m.ErrDashboardFolderCannotHaveParent ||
+		err == m.ErrDashboardFolderNameExists {
 		return ApiError(400, err.Error(), nil)
 		return ApiError(400, err.Error(), nil)
 	}
 	}
 
 
+	if err == m.ErrDashboardUpdateAccessDenied {
+		return ApiError(403, err.Error(), err)
+	}
+
 	if err == m.ErrDashboardContainsInvalidAlertData {
 	if err == m.ErrDashboardContainsInvalidAlertData {
 		return ApiError(500, "Invalid alert data. Cannot save dashboard", err)
 		return ApiError(500, "Invalid alert data. Cannot save dashboard", err)
 	}
 	}
 
 
 	if err != nil {
 	if err != nil {
-		if err == m.ErrDashboardWithSameUIDExists {
-			return Json(412, util.DynMap{"status": "name-exists", "message": err.Error()})
-		}
 		if err == m.ErrDashboardWithSameNameInFolderExists {
 		if err == m.ErrDashboardWithSameNameInFolderExists {
 			return Json(412, util.DynMap{"status": "name-exists", "message": err.Error()})
 			return Json(412, util.DynMap{"status": "name-exists", "message": err.Error()})
 		}
 		}
@@ -281,8 +262,6 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
 		return ApiError(500, "Invalid alert data. Cannot save dashboard", err)
 		return ApiError(500, "Invalid alert data. Cannot save dashboard", err)
 	}
 	}
 
 
-	dashboard.IsFolder = dash.IsFolder
-
 	c.TimeRequest(metrics.M_Api_Dashboard_Save)
 	c.TimeRequest(metrics.M_Api_Dashboard_Save)
 	return Json(200, util.DynMap{
 	return Json(200, util.DynMap{
 		"status":  "success",
 		"status":  "success",
@@ -357,7 +336,7 @@ func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) {
 func GetDashboardVersions(c *middleware.Context) Response {
 func GetDashboardVersions(c *middleware.Context) Response {
 	dashId := c.ParamsInt64(":dashboardId")
 	dashId := c.ParamsInt64(":dashboardId")
 
 
-	guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
+	guardian := guardian.New(dashId, c.OrgId, c.SignedInUser)
 	if canSave, err := guardian.CanSave(); err != nil || !canSave {
 	if canSave, err := guardian.CanSave(); err != nil || !canSave {
 		return dashboardGuardianResponse(err)
 		return dashboardGuardianResponse(err)
 	}
 	}
@@ -396,7 +375,7 @@ func GetDashboardVersions(c *middleware.Context) Response {
 func GetDashboardVersion(c *middleware.Context) Response {
 func GetDashboardVersion(c *middleware.Context) Response {
 	dashId := c.ParamsInt64(":dashboardId")
 	dashId := c.ParamsInt64(":dashboardId")
 
 
-	guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
+	guardian := guardian.New(dashId, c.OrgId, c.SignedInUser)
 	if canSave, err := guardian.CanSave(); err != nil || !canSave {
 	if canSave, err := guardian.CanSave(); err != nil || !canSave {
 		return dashboardGuardianResponse(err)
 		return dashboardGuardianResponse(err)
 	}
 	}
@@ -464,7 +443,7 @@ func RestoreDashboardVersion(c *middleware.Context, apiCmd dtos.RestoreDashboard
 		return rsp
 		return rsp
 	}
 	}
 
 
-	guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
+	guardian := guardian.New(dash.Id, c.OrgId, c.SignedInUser)
 	if canSave, err := guardian.CanSave(); err != nil || !canSave {
 	if canSave, err := guardian.CanSave(); err != nil || !canSave {
 		return dashboardGuardianResponse(err)
 		return dashboardGuardianResponse(err)
 	}
 	}
@@ -498,19 +477,3 @@ func GetDashboardTags(c *middleware.Context) {
 
 
 	c.JSON(200, query.Result)
 	c.JSON(200, query.Result)
 }
 }
-
-func GetFoldersForSignedInUser(c *middleware.Context) Response {
-	title := c.Query("query")
-	query := m.GetFoldersForSignedInUserQuery{
-		OrgId:        c.OrgId,
-		SignedInUser: c.SignedInUser,
-		Title:        title,
-	}
-
-	err := bus.Dispatch(&query)
-	if err != nil {
-		return ApiError(500, "Failed to get folders from database", err)
-	}
-
-	return Json(200, query.Result)
-}

+ 2 - 32
pkg/api/dashboard_acl.go

@@ -18,7 +18,7 @@ func GetDashboardAclList(c *middleware.Context) Response {
 		return rsp
 		return rsp
 	}
 	}
 
 
-	guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
+	guardian := guardian.New(dashId, c.OrgId, c.SignedInUser)
 
 
 	if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
 	if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
 		return dashboardGuardianResponse(err)
 		return dashboardGuardianResponse(err)
@@ -46,7 +46,7 @@ func UpdateDashboardAcl(c *middleware.Context, apiCmd dtos.UpdateDashboardAclCom
 		return rsp
 		return rsp
 	}
 	}
 
 
-	guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
+	guardian := guardian.New(dashId, c.OrgId, c.SignedInUser)
 	if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
 	if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
 		return dashboardGuardianResponse(err)
 		return dashboardGuardianResponse(err)
 	}
 	}
@@ -84,33 +84,3 @@ func UpdateDashboardAcl(c *middleware.Context, apiCmd dtos.UpdateDashboardAclCom
 
 
 	return ApiSuccess("Dashboard acl updated")
 	return ApiSuccess("Dashboard acl updated")
 }
 }
-
-func DeleteDashboardAcl(c *middleware.Context) Response {
-	dashId := c.ParamsInt64(":dashboardId")
-	aclId := c.ParamsInt64(":aclId")
-
-	_, rsp := getDashboardHelper(c.OrgId, "", dashId, "")
-	if rsp != nil {
-		return rsp
-	}
-
-	guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
-	if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
-		return dashboardGuardianResponse(err)
-	}
-
-	if okToDelete, err := guardian.CheckPermissionBeforeRemove(m.PERMISSION_ADMIN, aclId); err != nil || !okToDelete {
-		if err != nil {
-			return ApiError(500, "Error while checking dashboard permissions", err)
-		}
-
-		return ApiError(403, "Cannot remove own admin permission for a folder", nil)
-	}
-
-	cmd := m.RemoveDashboardAclCommand{OrgId: c.OrgId, AclId: aclId}
-	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "Failed to delete permission for user", err)
-	}
-
-	return Json(200, "")
-}

+ 9 - 95
pkg/api/dashboard_acl_test.go

@@ -15,11 +15,11 @@ import (
 func TestDashboardAclApiEndpoint(t *testing.T) {
 func TestDashboardAclApiEndpoint(t *testing.T) {
 	Convey("Given a dashboard acl", t, func() {
 	Convey("Given a dashboard acl", t, func() {
 		mockResult := []*m.DashboardAclInfoDTO{
 		mockResult := []*m.DashboardAclInfoDTO{
-			{Id: 1, OrgId: 1, DashboardId: 1, UserId: 2, Permission: m.PERMISSION_VIEW},
-			{Id: 2, OrgId: 1, DashboardId: 1, UserId: 3, Permission: m.PERMISSION_EDIT},
-			{Id: 3, OrgId: 1, DashboardId: 1, UserId: 4, Permission: m.PERMISSION_ADMIN},
-			{Id: 4, OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_VIEW},
-			{Id: 5, OrgId: 1, DashboardId: 1, TeamId: 2, Permission: m.PERMISSION_ADMIN},
+			{OrgId: 1, DashboardId: 1, UserId: 2, Permission: m.PERMISSION_VIEW},
+			{OrgId: 1, DashboardId: 1, UserId: 3, Permission: m.PERMISSION_EDIT},
+			{OrgId: 1, DashboardId: 1, UserId: 4, Permission: m.PERMISSION_ADMIN},
+			{OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_VIEW},
+			{OrgId: 1, DashboardId: 1, TeamId: 2, Permission: m.PERMISSION_ADMIN},
 		}
 		}
 		dtoRes := transformDashboardAclsToDTOs(mockResult)
 		dtoRes := transformDashboardAclsToDTOs(mockResult)
 
 
@@ -92,21 +92,11 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
 					So(sc.resp.Code, ShouldEqual, 404)
 					So(sc.resp.Code, ShouldEqual, 404)
 				})
 				})
 			})
 			})
-
-			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/2/acl/6", "/api/dashboards/id/:dashboardId/acl/:aclId", m.ROLE_ADMIN, func(sc *scenarioContext) {
-				getDashboardNotFoundError = m.ErrDashboardNotFound
-				sc.handlerFunc = DeleteDashboardAcl
-				sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
-
-				Convey("Should not be able to delete non-existing dashboard", func() {
-					So(sc.resp.Code, ShouldEqual, 404)
-				})
-			})
 		})
 		})
 
 
 		Convey("When user is org editor and has admin permission in the ACL", func() {
 		Convey("When user is org editor and has admin permission in the ACL", func() {
 			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, func(sc *scenarioContext) {
 			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, func(sc *scenarioContext) {
-				mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 6, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
+				mockResult = append(mockResult, &m.DashboardAclInfoDTO{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
 
 
 				Convey("Should be able to access ACL", func() {
 				Convey("Should be able to access ACL", func() {
 					sc.handlerFunc = GetDashboardAclList
 					sc.handlerFunc = GetDashboardAclList
@@ -116,36 +106,6 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
 				})
 				})
 			})
 			})
 
 
-			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) {
-				mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 6, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
-
-				bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error {
-					return nil
-				})
-
-				Convey("Should be able to delete permission", func() {
-					sc.handlerFunc = DeleteDashboardAcl
-					sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
-
-					So(sc.resp.Code, ShouldEqual, 200)
-				})
-			})
-
-			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/6", "/api/dashboards/id/:dashboardId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) {
-				mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 6, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
-
-				bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error {
-					return nil
-				})
-
-				Convey("Should not be able to delete their own Admin permission", func() {
-					sc.handlerFunc = DeleteDashboardAcl
-					sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
-
-					So(sc.resp.Code, ShouldEqual, 403)
-				})
-			})
-
 			Convey("Should not be able to downgrade their own Admin permission", func() {
 			Convey("Should not be able to downgrade their own Admin permission", func() {
 				cmd := dtos.UpdateDashboardAclCommand{
 				cmd := dtos.UpdateDashboardAclCommand{
 					Items: []dtos.DashboardAclUpdateItem{
 					Items: []dtos.DashboardAclUpdateItem{
@@ -154,7 +114,7 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
 				}
 				}
 
 
 				postAclScenario("When calling POST on", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, cmd, func(sc *scenarioContext) {
 				postAclScenario("When calling POST on", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, cmd, func(sc *scenarioContext) {
-					mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 6, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
+					mockResult = append(mockResult, &m.DashboardAclInfoDTO{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
 
 
 					CallPostAcl(sc)
 					CallPostAcl(sc)
 					So(sc.resp.Code, ShouldEqual, 403)
 					So(sc.resp.Code, ShouldEqual, 403)
@@ -170,34 +130,18 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
 				}
 				}
 
 
 				postAclScenario("When calling POST on", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, cmd, func(sc *scenarioContext) {
 				postAclScenario("When calling POST on", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, cmd, func(sc *scenarioContext) {
-					mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 6, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
+					mockResult = append(mockResult, &m.DashboardAclInfoDTO{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
 
 
 					CallPostAcl(sc)
 					CallPostAcl(sc)
 					So(sc.resp.Code, ShouldEqual, 200)
 					So(sc.resp.Code, ShouldEqual, 200)
 				})
 				})
 			})
 			})
 
 
-			Convey("When user is a member of a team in the ACL with admin permission", func() {
-				loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardsId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) {
-					teamResp = append(teamResp, &m.Team{Id: 2, OrgId: 1, Name: "UG2"})
-
-					bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error {
-						return nil
-					})
-
-					Convey("Should be able to delete permission", func() {
-						sc.handlerFunc = DeleteDashboardAcl
-						sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
-
-						So(sc.resp.Code, ShouldEqual, 200)
-					})
-				})
-			})
 		})
 		})
 
 
 		Convey("When user is org viewer and has edit permission in the ACL", func() {
 		Convey("When user is org viewer and has edit permission in the ACL", func() {
 			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_VIEWER, func(sc *scenarioContext) {
 			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_VIEWER, func(sc *scenarioContext) {
-				mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_EDIT})
+				mockResult = append(mockResult, &m.DashboardAclInfoDTO{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_EDIT})
 
 
 				// Getting the permissions is an Admin permission
 				// Getting the permissions is an Admin permission
 				Convey("Should not be able to get list of permissions from ACL", func() {
 				Convey("Should not be able to get list of permissions from ACL", func() {
@@ -207,21 +151,6 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
 					So(sc.resp.Code, ShouldEqual, 403)
 					So(sc.resp.Code, ShouldEqual, 403)
 				})
 				})
 			})
 			})
-
-			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardId/acl/:aclId", m.ROLE_VIEWER, func(sc *scenarioContext) {
-				mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_EDIT})
-
-				bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error {
-					return nil
-				})
-
-				Convey("Should be not be able to delete permission", func() {
-					sc.handlerFunc = DeleteDashboardAcl
-					sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
-
-					So(sc.resp.Code, ShouldEqual, 403)
-				})
-			})
 		})
 		})
 
 
 		Convey("When user is org editor and not in the ACL", func() {
 		Convey("When user is org editor and not in the ACL", func() {
@@ -234,20 +163,6 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
 					So(sc.resp.Code, ShouldEqual, 403)
 					So(sc.resp.Code, ShouldEqual, 403)
 				})
 				})
 			})
 			})
-
-			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/user/1", "/api/dashboards/id/:dashboardsId/acl/user/:userId", m.ROLE_EDITOR, func(sc *scenarioContext) {
-				mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_VIEW})
-				bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error {
-					return nil
-				})
-
-				Convey("Should be not be able to delete permission", func() {
-					sc.handlerFunc = DeleteDashboardAcl
-					sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
-
-					So(sc.resp.Code, ShouldEqual, 403)
-				})
-			})
 		})
 		})
 	})
 	})
 }
 }
@@ -257,7 +172,6 @@ func transformDashboardAclsToDTOs(acls []*m.DashboardAclInfoDTO) []*m.DashboardA
 
 
 	for _, acl := range acls {
 	for _, acl := range acls {
 		dto := &m.DashboardAclInfoDTO{
 		dto := &m.DashboardAclInfoDTO{
-			Id:          acl.Id,
 			OrgId:       acl.OrgId,
 			OrgId:       acl.OrgId,
 			DashboardId: acl.DashboardId,
 			DashboardId: acl.DashboardId,
 			Permission:  acl.Permission,
 			Permission:  acl.Permission,

+ 116 - 116
pkg/api/dashboard_test.go

@@ -2,6 +2,7 @@ package api
 
 
 import (
 import (
 	"encoding/json"
 	"encoding/json"
+	"fmt"
 	"testing"
 	"testing"
 
 
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/api/dtos"
@@ -9,28 +10,17 @@ import (
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/services/alerting"
 	"github.com/grafana/grafana/pkg/services/dashboards"
 	"github.com/grafana/grafana/pkg/services/dashboards"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
 
 
 	. "github.com/smartystreets/goconvey/convey"
 	. "github.com/smartystreets/goconvey/convey"
 )
 )
 
 
-type fakeDashboardRepo struct {
-	inserted     []*dashboards.SaveDashboardItem
-	getDashboard []*m.Dashboard
-}
-
-func (repo *fakeDashboardRepo) SaveDashboard(json *dashboards.SaveDashboardItem) (*m.Dashboard, error) {
-	repo.inserted = append(repo.inserted, json)
-	return json.Dashboard, nil
-}
-
-var fakeRepo *fakeDashboardRepo
-
-// This tests two main scenarios. If a user has access to execute an action on a dashboard:
-// 1. and the dashboard is in a folder which does not have an acl
-// 2. and the dashboard is in a folder which does have an acl
+// This tests three main scenarios.
+// If a user has access to execute an action on a dashboard:
+//   1. and the dashboard is in a folder which does not have an acl
+//   2. and the dashboard is in a folder which does have an acl
+// 3. Post dashboard response tests
 
 
 func TestDashboardApiEndpoint(t *testing.T) {
 func TestDashboardApiEndpoint(t *testing.T) {
 	Convey("Given a dashboard with a parent folder which does not have an acl", t, func() {
 	Convey("Given a dashboard with a parent folder which does not have an acl", t, func() {
@@ -71,14 +61,6 @@ func TestDashboardApiEndpoint(t *testing.T) {
 			return nil
 			return nil
 		})
 		})
 
 
-		cmd := m.SaveDashboardCommand{
-			Dashboard: simplejson.NewFromAny(map[string]interface{}{
-				"folderId": fakeDash.FolderId,
-				"title":    fakeDash.Title,
-				"id":       fakeDash.Id,
-			}),
-		}
-
 		// This tests two scenarios:
 		// This tests two scenarios:
 		// 1. user is an org viewer
 		// 1. user is an org viewer
 		// 2. user is an org editor
 		// 2. user is an org editor
@@ -141,11 +123,6 @@ func TestDashboardApiEndpoint(t *testing.T) {
 				CallGetDashboardVersions(sc)
 				CallGetDashboardVersions(sc)
 				So(sc.resp.Code, ShouldEqual, 403)
 				So(sc.resp.Code, ShouldEqual, 403)
 			})
 			})
-
-			postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
-				CallPostDashboard(sc)
-				So(sc.resp.Code, ShouldEqual, 403)
-			})
 		})
 		})
 
 
 		Convey("When user is an Org Editor", func() {
 		Convey("When user is an Org Editor", func() {
@@ -206,32 +183,6 @@ func TestDashboardApiEndpoint(t *testing.T) {
 				CallGetDashboardVersions(sc)
 				CallGetDashboardVersions(sc)
 				So(sc.resp.Code, ShouldEqual, 200)
 				So(sc.resp.Code, ShouldEqual, 200)
 			})
 			})
-
-			postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
-				CallPostDashboardShouldReturnSuccess(sc)
-			})
-
-			Convey("When saving a dashboard folder in another folder", func() {
-				bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
-					query.Result = fakeDash
-					query.Result.IsFolder = true
-					return nil
-				})
-				invalidCmd := m.SaveDashboardCommand{
-					FolderId: fakeDash.FolderId,
-					IsFolder: true,
-					Dashboard: simplejson.NewFromAny(map[string]interface{}{
-						"folderId": fakeDash.FolderId,
-						"title":    fakeDash.Title,
-					}),
-				}
-				Convey("Should return an error", func() {
-					postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, invalidCmd, func(sc *scenarioContext) {
-						CallPostDashboard(sc)
-						So(sc.resp.Code, ShouldEqual, 400)
-					})
-				})
-			})
 		})
 		})
 	})
 	})
 
 
@@ -274,15 +225,6 @@ func TestDashboardApiEndpoint(t *testing.T) {
 			return nil
 			return nil
 		})
 		})
 
 
-		cmd := m.SaveDashboardCommand{
-			FolderId: fakeDash.FolderId,
-			Dashboard: simplejson.NewFromAny(map[string]interface{}{
-				"id":       fakeDash.Id,
-				"folderId": fakeDash.FolderId,
-				"title":    fakeDash.Title,
-			}),
-		}
-
 		// This tests six scenarios:
 		// This tests six scenarios:
 		// 1. user is an org viewer AND has no permissions for this dashboard
 		// 1. user is an org viewer AND has no permissions for this dashboard
 		// 2. user is an org editor AND has no permissions for this dashboard
 		// 2. user is an org editor AND has no permissions for this dashboard
@@ -347,11 +289,6 @@ func TestDashboardApiEndpoint(t *testing.T) {
 				CallGetDashboardVersions(sc)
 				CallGetDashboardVersions(sc)
 				So(sc.resp.Code, ShouldEqual, 403)
 				So(sc.resp.Code, ShouldEqual, 403)
 			})
 			})
-
-			postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
-				CallPostDashboard(sc)
-				So(sc.resp.Code, ShouldEqual, 403)
-			})
 		})
 		})
 
 
 		Convey("When user is an Org Editor and has no permissions for this dashboard", func() {
 		Convey("When user is an Org Editor and has no permissions for this dashboard", func() {
@@ -410,18 +347,13 @@ func TestDashboardApiEndpoint(t *testing.T) {
 				CallGetDashboardVersions(sc)
 				CallGetDashboardVersions(sc)
 				So(sc.resp.Code, ShouldEqual, 403)
 				So(sc.resp.Code, ShouldEqual, 403)
 			})
 			})
-
-			postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
-				CallPostDashboard(sc)
-				So(sc.resp.Code, ShouldEqual, 403)
-			})
 		})
 		})
 
 
 		Convey("When user is an Org Viewer but has an edit permission", func() {
 		Convey("When user is an Org Viewer but has an edit permission", func() {
 			role := m.ROLE_VIEWER
 			role := m.ROLE_VIEWER
 
 
 			mockResult := []*m.DashboardAclInfoDTO{
 			mockResult := []*m.DashboardAclInfoDTO{
-				{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_EDIT},
+				{OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_EDIT},
 			}
 			}
 
 
 			bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
 			bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
@@ -484,10 +416,6 @@ func TestDashboardApiEndpoint(t *testing.T) {
 				CallGetDashboardVersions(sc)
 				CallGetDashboardVersions(sc)
 				So(sc.resp.Code, ShouldEqual, 200)
 				So(sc.resp.Code, ShouldEqual, 200)
 			})
 			})
-
-			postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
-				CallPostDashboardShouldReturnSuccess(sc)
-			})
 		})
 		})
 
 
 		Convey("When user is an Org Viewer and viewers can edit", func() {
 		Convey("When user is an Org Viewer and viewers can edit", func() {
@@ -495,7 +423,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
 			setting.ViewersCanEdit = true
 			setting.ViewersCanEdit = true
 
 
 			mockResult := []*m.DashboardAclInfoDTO{
 			mockResult := []*m.DashboardAclInfoDTO{
-				{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW},
+				{OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW},
 			}
 			}
 
 
 			bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
 			bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
@@ -554,7 +482,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
 			role := m.ROLE_VIEWER
 			role := m.ROLE_VIEWER
 
 
 			mockResult := []*m.DashboardAclInfoDTO{
 			mockResult := []*m.DashboardAclInfoDTO{
-				{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_ADMIN},
+				{OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_ADMIN},
 			}
 			}
 
 
 			bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
 			bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
@@ -617,17 +545,13 @@ func TestDashboardApiEndpoint(t *testing.T) {
 				CallGetDashboardVersions(sc)
 				CallGetDashboardVersions(sc)
 				So(sc.resp.Code, ShouldEqual, 200)
 				So(sc.resp.Code, ShouldEqual, 200)
 			})
 			})
-
-			postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
-				CallPostDashboardShouldReturnSuccess(sc)
-			})
 		})
 		})
 
 
 		Convey("When user is an Org Editor but has a view permission", func() {
 		Convey("When user is an Org Editor but has a view permission", func() {
 			role := m.ROLE_EDITOR
 			role := m.ROLE_EDITOR
 
 
 			mockResult := []*m.DashboardAclInfoDTO{
 			mockResult := []*m.DashboardAclInfoDTO{
-				{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW},
+				{OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW},
 			}
 			}
 
 
 			bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
 			bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
@@ -688,11 +612,6 @@ func TestDashboardApiEndpoint(t *testing.T) {
 				CallGetDashboardVersions(sc)
 				CallGetDashboardVersions(sc)
 				So(sc.resp.Code, ShouldEqual, 403)
 				So(sc.resp.Code, ShouldEqual, 403)
 			})
 			})
-
-			postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
-				CallPostDashboard(sc)
-				So(sc.resp.Code, ShouldEqual, 403)
-			})
 		})
 		})
 	})
 	})
 
 
@@ -726,6 +645,104 @@ func TestDashboardApiEndpoint(t *testing.T) {
 			})
 			})
 		})
 		})
 	})
 	})
+
+	Convey("Post dashboard response tests", t, func() {
+
+		// This tests that a valid request returns correct response
+
+		Convey("Given a correct request for creating a dashboard", func() {
+			cmd := m.SaveDashboardCommand{
+				OrgId:  1,
+				UserId: 5,
+				Dashboard: simplejson.NewFromAny(map[string]interface{}{
+					"title": "Dash",
+				}),
+				Overwrite: true,
+				FolderId:  3,
+				IsFolder:  false,
+				Message:   "msg",
+			}
+
+			mock := &dashboards.FakeDashboardService{
+				SaveDashboardResult: &m.Dashboard{
+					Id:      2,
+					Uid:     "uid",
+					Title:   "Dash",
+					Slug:    "dash",
+					Version: 2,
+				},
+			}
+
+			postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", mock, cmd, func(sc *scenarioContext) {
+				CallPostDashboardShouldReturnSuccess(sc)
+
+				Convey("It should call dashboard service with correct data", func() {
+					dto := mock.SavedDashboards[0]
+					So(dto.OrgId, ShouldEqual, cmd.OrgId)
+					So(dto.User.UserId, ShouldEqual, cmd.UserId)
+					So(dto.Dashboard.FolderId, ShouldEqual, 3)
+					So(dto.Dashboard.Title, ShouldEqual, "Dash")
+					So(dto.Overwrite, ShouldBeTrue)
+					So(dto.Message, ShouldEqual, "msg")
+				})
+
+				Convey("It should return correct response data", func() {
+					result := sc.ToJson()
+					So(result.Get("status").MustString(), ShouldEqual, "success")
+					So(result.Get("id").MustInt64(), ShouldEqual, 2)
+					So(result.Get("uid").MustString(), ShouldEqual, "uid")
+					So(result.Get("slug").MustString(), ShouldEqual, "dash")
+					So(result.Get("url").MustString(), ShouldEqual, "/d/uid/dash")
+				})
+			})
+		})
+
+		// This tests that invalid requests returns expected error responses
+
+		Convey("Given incorrect requests for creating a dashboard", func() {
+			testCases := []struct {
+				SaveError          error
+				ExpectedStatusCode int
+			}{
+				{SaveError: m.ErrDashboardNotFound, ExpectedStatusCode: 404},
+				{SaveError: m.ErrFolderNotFound, ExpectedStatusCode: 400},
+				{SaveError: m.ErrDashboardWithSameUIDExists, ExpectedStatusCode: 400},
+				{SaveError: m.ErrDashboardWithSameNameInFolderExists, ExpectedStatusCode: 412},
+				{SaveError: m.ErrDashboardVersionMismatch, ExpectedStatusCode: 412},
+				{SaveError: m.ErrDashboardTitleEmpty, ExpectedStatusCode: 400},
+				{SaveError: m.ErrDashboardFolderCannotHaveParent, ExpectedStatusCode: 400},
+				{SaveError: m.ErrDashboardContainsInvalidAlertData, ExpectedStatusCode: 500},
+				{SaveError: m.ErrDashboardFailedToUpdateAlertData, ExpectedStatusCode: 500},
+				{SaveError: m.ErrDashboardFailedGenerateUniqueUid, ExpectedStatusCode: 500},
+				{SaveError: m.ErrDashboardTypeMismatch, ExpectedStatusCode: 400},
+				{SaveError: m.ErrDashboardFolderWithSameNameAsDashboard, ExpectedStatusCode: 400},
+				{SaveError: m.ErrDashboardWithSameNameAsFolder, ExpectedStatusCode: 400},
+				{SaveError: m.ErrDashboardFolderNameExists, ExpectedStatusCode: 400},
+				{SaveError: m.ErrDashboardUpdateAccessDenied, ExpectedStatusCode: 403},
+				{SaveError: m.ErrDashboardInvalidUid, ExpectedStatusCode: 400},
+				{SaveError: m.ErrDashboardUidToLong, ExpectedStatusCode: 400},
+				{SaveError: m.UpdatePluginDashboardError{PluginId: "plug"}, ExpectedStatusCode: 412},
+			}
+
+			cmd := m.SaveDashboardCommand{
+				OrgId: 1,
+				Dashboard: simplejson.NewFromAny(map[string]interface{}{
+					"title": "",
+				}),
+			}
+
+			for _, tc := range testCases {
+				mock := &dashboards.FakeDashboardService{
+					SaveDashboardError: tc.SaveError,
+				}
+
+				postDashboardScenario(fmt.Sprintf("Expect '%s' error when calling POST on", tc.SaveError.Error()), "/api/dashboards", "/api/dashboards", mock, cmd, func(sc *scenarioContext) {
+					CallPostDashboard(sc)
+					So(sc.resp.Code, ShouldEqual, tc.ExpectedStatusCode)
+				})
+			}
+		})
+	})
 }
 }
 
 
 func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta {
 func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta {
@@ -780,19 +797,6 @@ func CallDeleteDashboardByUid(sc *scenarioContext) {
 }
 }
 
 
 func CallPostDashboard(sc *scenarioContext) {
 func CallPostDashboard(sc *scenarioContext) {
-	bus.AddHandler("test", func(cmd *alerting.ValidateDashboardAlertsCommand) error {
-		return nil
-	})
-
-	bus.AddHandler("test", func(cmd *m.SaveDashboardCommand) error {
-		cmd.Result = &m.Dashboard{Id: 2, Slug: "Dash", Version: 2}
-		return nil
-	})
-
-	bus.AddHandler("test", func(cmd *alerting.UpdateDashboardAlertsCommand) error {
-		return nil
-	})
-
 	sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
 	sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
 }
 }
 
 
@@ -800,33 +804,29 @@ func CallPostDashboardShouldReturnSuccess(sc *scenarioContext) {
 	CallPostDashboard(sc)
 	CallPostDashboard(sc)
 
 
 	So(sc.resp.Code, ShouldEqual, 200)
 	So(sc.resp.Code, ShouldEqual, 200)
-	result := sc.ToJson()
-	So(result.Get("status").MustString(), ShouldEqual, "success")
-	So(result.Get("id").MustInt64(), ShouldBeGreaterThan, 0)
-	So(result.Get("uid").MustString(), ShouldNotBeNil)
-	So(result.Get("slug").MustString(), ShouldNotBeNil)
-	So(result.Get("url").MustString(), ShouldNotBeNil)
 }
 }
 
 
-func postDashboardScenario(desc string, url string, routePattern string, role m.RoleType, cmd m.SaveDashboardCommand, fn scenarioFunc) {
+func postDashboardScenario(desc string, url string, routePattern string, mock *dashboards.FakeDashboardService, cmd m.SaveDashboardCommand, fn scenarioFunc) {
 	Convey(desc+" "+url, func() {
 	Convey(desc+" "+url, func() {
 		defer bus.ClearBusHandlers()
 		defer bus.ClearBusHandlers()
 
 
 		sc := setupScenarioContext(url)
 		sc := setupScenarioContext(url)
 		sc.defaultHandler = wrap(func(c *middleware.Context) Response {
 		sc.defaultHandler = wrap(func(c *middleware.Context) Response {
 			sc.context = c
 			sc.context = c
-			sc.context.UserId = TestUserID
-			sc.context.OrgId = TestOrgID
-			sc.context.OrgRole = role
+			sc.context.SignedInUser = &m.SignedInUser{OrgId: cmd.OrgId, UserId: cmd.UserId}
 
 
 			return PostDashboard(c, cmd)
 			return PostDashboard(c, cmd)
 		})
 		})
 
 
-		fakeRepo = &fakeDashboardRepo{}
-		dashboards.SetRepository(fakeRepo)
+		origNewDashboardService := dashboards.NewService
+		dashboards.MockDashboardService(mock)
 
 
 		sc.m.Post(routePattern, sc.defaultHandler)
 		sc.m.Post(routePattern, sc.defaultHandler)
 
 
+		defer func() {
+			dashboards.NewService = origNewDashboardService
+		}()
+
 		fn(sc)
 		fn(sc)
 	})
 	})
 }
 }

+ 1 - 1
pkg/api/plugins.go

@@ -168,7 +168,7 @@ func ImportDashboard(c *middleware.Context, apiCmd dtos.ImportDashboardCommand)
 
 
 	cmd := plugins.ImportDashboardCommand{
 	cmd := plugins.ImportDashboardCommand{
 		OrgId:     c.OrgId,
 		OrgId:     c.OrgId,
-		UserId:    c.UserId,
+		User:      c.SignedInUser,
 		PluginId:  apiCmd.PluginId,
 		PluginId:  apiCmd.PluginId,
 		Path:      apiCmd.Path,
 		Path:      apiCmd.Path,
 		Inputs:    apiCmd.Inputs,
 		Inputs:    apiCmd.Inputs,

+ 7 - 0
pkg/api/search.go

@@ -6,6 +6,7 @@ import (
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/middleware"
+	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/services/search"
 	"github.com/grafana/grafana/pkg/services/search"
 )
 )
 
 
@@ -15,11 +16,16 @@ func Search(c *middleware.Context) {
 	starred := c.Query("starred")
 	starred := c.Query("starred")
 	limit := c.QueryInt("limit")
 	limit := c.QueryInt("limit")
 	dashboardType := c.Query("type")
 	dashboardType := c.Query("type")
+	permission := models.PERMISSION_VIEW
 
 
 	if limit == 0 {
 	if limit == 0 {
 		limit = 1000
 		limit = 1000
 	}
 	}
 
 
+	if c.Query("permission") == "Edit" {
+		permission = models.PERMISSION_EDIT
+	}
+
 	dbids := make([]int64, 0)
 	dbids := make([]int64, 0)
 	for _, id := range c.QueryStrings("dashboardIds") {
 	for _, id := range c.QueryStrings("dashboardIds") {
 		dashboardId, err := strconv.ParseInt(id, 10, 64)
 		dashboardId, err := strconv.ParseInt(id, 10, 64)
@@ -46,6 +52,7 @@ func Search(c *middleware.Context) {
 		DashboardIds: dbids,
 		DashboardIds: dbids,
 		Type:         dashboardType,
 		Type:         dashboardType,
 		FolderIds:    folderIds,
 		FolderIds:    folderIds,
+		Permission:   permission,
 	}
 	}
 
 
 	err := bus.Dispatch(&searchQuery)
 	err := bus.Dispatch(&searchQuery)

+ 14 - 1
pkg/api/team_members.go

@@ -29,9 +29,14 @@ func AddTeamMember(c *middleware.Context, cmd m.AddTeamMemberCommand) Response {
 	cmd.OrgId = c.OrgId
 	cmd.OrgId = c.OrgId
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
+		if err == m.ErrTeamNotFound {
+			return ApiError(404, "Team not found", nil)
+		}
+
 		if err == m.ErrTeamMemberAlreadyAdded {
 		if err == m.ErrTeamMemberAlreadyAdded {
-			return ApiError(400, "User is already added to this team", err)
+			return ApiError(400, "User is already added to this team", nil)
 		}
 		}
+
 		return ApiError(500, "Failed to add Member to Team", err)
 		return ApiError(500, "Failed to add Member to Team", err)
 	}
 	}
 
 
@@ -43,6 +48,14 @@ func AddTeamMember(c *middleware.Context, cmd m.AddTeamMemberCommand) Response {
 // DELETE /api/teams/:teamId/members/:userId
 // DELETE /api/teams/:teamId/members/:userId
 func RemoveTeamMember(c *middleware.Context) Response {
 func RemoveTeamMember(c *middleware.Context) Response {
 	if err := bus.Dispatch(&m.RemoveTeamMemberCommand{OrgId: c.OrgId, TeamId: c.ParamsInt64(":teamId"), UserId: c.ParamsInt64(":userId")}); err != nil {
 	if err := bus.Dispatch(&m.RemoveTeamMemberCommand{OrgId: c.OrgId, TeamId: c.ParamsInt64(":teamId"), UserId: c.ParamsInt64(":userId")}); err != nil {
+		if err == m.ErrTeamNotFound {
+			return ApiError(404, "Team not found", nil)
+		}
+
+		if err == m.ErrTeamMemberNotFound {
+			return ApiError(404, "Team member not found", nil)
+		}
+
 		return ApiError(500, "Failed to remove Member from Team", err)
 		return ApiError(500, "Failed to remove Member from Team", err)
 	}
 	}
 	return ApiSuccess("Team Member removed")
 	return ApiSuccess("Team Member removed")

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

@@ -94,7 +94,7 @@ func InstallPlugin(pluginName, version string, c CommandLine) error {
 
 
 	res, _ := s.ReadPlugin(pluginFolder, pluginName)
 	res, _ := s.ReadPlugin(pluginFolder, pluginName)
 	for _, v := range res.Dependencies.Plugins {
 	for _, v := range res.Dependencies.Plugins {
-		InstallPlugin(v.Id, version, c)
+		InstallPlugin(v.Id, "", c)
 		logger.Infof("Installed dependency: %v ✔\n", v.Id)
 		logger.Infof("Installed dependency: %v ✔\n", v.Id)
 	}
 	}
 
 

+ 2 - 1
pkg/middleware/auth.go

@@ -51,7 +51,8 @@ func notAuthorized(c *Context) {
 		return
 		return
 	}
 	}
 
 
-	c.SetCookie("redirect_to", url.QueryEscape(setting.AppSubUrl+c.Req.RequestURI), 0, setting.AppSubUrl+"/")
+	c.SetCookie("redirect_to", url.QueryEscape(setting.AppSubUrl+c.Req.RequestURI), 0, setting.AppSubUrl+"/", nil, false, true)
+
 	c.Redirect(setting.AppSubUrl + "/login")
 	c.Redirect(setting.AppSubUrl + "/login")
 }
 }
 
 

+ 4 - 4
pkg/middleware/recovery.go

@@ -115,11 +115,11 @@ func Recovery() macaron.Handler {
 				c.Data["Title"] = "Server Error"
 				c.Data["Title"] = "Server Error"
 				c.Data["AppSubUrl"] = setting.AppSubUrl
 				c.Data["AppSubUrl"] = setting.AppSubUrl
 
 
-				if theErr, ok := err.(error); ok {
-					c.Data["Title"] = theErr.Error()
-				}
-
 				if setting.Env == setting.DEV {
 				if setting.Env == setting.DEV {
+					if theErr, ok := err.(error); ok {
+						c.Data["Title"] = theErr.Error()
+					}
+
 					c.Data["ErrorMsg"] = string(stack)
 					c.Data["ErrorMsg"] = string(stack)
 				}
 				}
 
 

+ 31 - 1
pkg/models/alert.go

@@ -166,8 +166,9 @@ type GetAlertsQuery struct {
 	DashboardId int64
 	DashboardId int64
 	PanelId     int64
 	PanelId     int64
 	Limit       int64
 	Limit       int64
+	User        *SignedInUser
 
 
-	Result []*Alert
+	Result []*AlertListItemDTO
 }
 }
 
 
 type GetAllAlertsQuery struct {
 type GetAllAlertsQuery struct {
@@ -187,6 +188,21 @@ type GetAlertStatesForDashboardQuery struct {
 	Result []*AlertStateInfoDTO
 	Result []*AlertStateInfoDTO
 }
 }
 
 
+type AlertListItemDTO struct {
+	Id             int64            `json:"id"`
+	DashboardId    int64            `json:"dashboardId"`
+	DashboardUid   string           `json:"dashboardUid"`
+	DashboardSlug  string           `json:"dashboardSlug"`
+	PanelId        int64            `json:"panelId"`
+	Name           string           `json:"name"`
+	State          AlertStateType   `json:"state"`
+	NewStateDate   time.Time        `json:"newStateDate"`
+	EvalDate       time.Time        `json:"evalDate"`
+	EvalData       *simplejson.Json `json:"evalData"`
+	ExecutionError string           `json:"executionError"`
+	Url            string           `json:"url"`
+}
+
 type AlertStateInfoDTO struct {
 type AlertStateInfoDTO struct {
 	Id           int64          `json:"id"`
 	Id           int64          `json:"id"`
 	DashboardId  int64          `json:"dashboardId"`
 	DashboardId  int64          `json:"dashboardId"`
@@ -194,3 +210,17 @@ type AlertStateInfoDTO struct {
 	State        AlertStateType `json:"state"`
 	State        AlertStateType `json:"state"`
 	NewStateDate time.Time      `json:"newStateDate"`
 	NewStateDate time.Time      `json:"newStateDate"`
 }
 }
+
+// "Internal" commands
+
+type UpdateDashboardAlertsCommand struct {
+	UserId    int64
+	OrgId     int64
+	Dashboard *Dashboard
+}
+
+type ValidateDashboardAlertsCommand struct {
+	UserId    int64
+	OrgId     int64
+	Dashboard *Dashboard
+}

+ 0 - 16
pkg/models/dashboard_acl.go

@@ -44,7 +44,6 @@ type DashboardAcl struct {
 }
 }
 
 
 type DashboardAclInfoDTO struct {
 type DashboardAclInfoDTO struct {
-	Id          int64 `json:"id"`
 	OrgId       int64 `json:"-"`
 	OrgId       int64 `json:"-"`
 	DashboardId int64 `json:"dashboardId"`
 	DashboardId int64 `json:"dashboardId"`
 
 
@@ -75,21 +74,6 @@ type UpdateDashboardAclCommand struct {
 	Items       []*DashboardAcl
 	Items       []*DashboardAcl
 }
 }
 
 
-type SetDashboardAclCommand struct {
-	DashboardId int64
-	OrgId       int64
-	UserId      int64
-	TeamId      int64
-	Permission  PermissionType
-
-	Result DashboardAcl
-}
-
-type RemoveDashboardAclCommand struct {
-	AclId int64
-	OrgId int64
-}
-
 //
 //
 // QUERIES
 // QUERIES
 //
 //

+ 72 - 28
pkg/models/dashboards.go

@@ -13,22 +13,26 @@ import (
 
 
 // Typed errors
 // Typed errors
 var (
 var (
-	ErrDashboardNotFound                        = errors.New("Dashboard 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")
-	ErrDashboardExistingCannotChangeToDashboard = errors.New("An existing folder cannot be changed to a dashboard")
-	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")
-	RootFolderName                              = "General"
+	ErrDashboardNotFound                      = errors.New("Dashboard not found")
+	ErrFolderNotFound                         = 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"
 )
 )
 
 
 type UpdatePluginDashboardError struct {
 type UpdatePluginDashboardError struct {
@@ -69,6 +73,30 @@ type Dashboard struct {
 	Data  *simplejson.Json
 	Data  *simplejson.Json
 }
 }
 
 
+func (d *Dashboard) SetId(id int64) {
+	d.Id = id
+	d.Data.Set("id", id)
+}
+
+func (d *Dashboard) SetUid(uid string) {
+	d.Uid = uid
+	d.Data.Set("uid", uid)
+}
+
+func (d *Dashboard) SetVersion(version int) {
+	d.Version = version
+	d.Data.Set("version", version)
+}
+
+// GetDashboardIdForSavePermissionCheck return the dashboard id to be used for checking permission of dashboard
+func (d *Dashboard) GetDashboardIdForSavePermissionCheck() int64 {
+	if d.Id == 0 {
+		return d.FolderId
+	}
+
+	return d.Id
+}
+
 // NewDashboard creates a new dashboard
 // NewDashboard creates a new dashboard
 func NewDashboard(title string) *Dashboard {
 func NewDashboard(title string) *Dashboard {
 	dash := &Dashboard{}
 	dash := &Dashboard{}
@@ -87,6 +115,7 @@ func NewDashboardFolder(title string) *Dashboard {
 	folder.Data.Set("schemaVersion", 16)
 	folder.Data.Set("schemaVersion", 16)
 	folder.Data.Set("editable", true)
 	folder.Data.Set("editable", true)
 	folder.Data.Set("hideControls", true)
 	folder.Data.Set("hideControls", true)
+	folder.IsFolder = true
 	return folder
 	return folder
 }
 }
 
 
@@ -219,11 +248,32 @@ type SaveDashboardCommand struct {
 	Result *Dashboard
 	Result *Dashboard
 }
 }
 
 
+type DashboardProvisioning struct {
+	Id          int64
+	DashboardId int64
+	Name        string
+	ExternalId  string
+	Updated     int64
+}
+
+type SaveProvisionedDashboardCommand struct {
+	DashboardCmd          *SaveDashboardCommand
+	DashboardProvisioning *DashboardProvisioning
+
+	Result *Dashboard
+}
+
 type DeleteDashboardCommand struct {
 type DeleteDashboardCommand struct {
 	Id    int64
 	Id    int64
 	OrgId int64
 	OrgId int64
 }
 }
 
 
+type ValidateDashboardBeforeSaveCommand struct {
+	OrgId     int64
+	Dashboard *Dashboard
+	Overwrite bool
+}
+
 //
 //
 // QUERIES
 // QUERIES
 //
 //
@@ -271,6 +321,12 @@ type GetDashboardSlugByIdQuery struct {
 	Result string
 	Result string
 }
 }
 
 
+type GetProvisionedDashboardDataQuery struct {
+	Name string
+
+	Result []*DashboardProvisioning
+}
+
 type GetDashboardsBySlugQuery struct {
 type GetDashboardsBySlugQuery struct {
 	OrgId int64
 	OrgId int64
 	Slug  string
 	Slug  string
@@ -278,18 +334,6 @@ type GetDashboardsBySlugQuery struct {
 	Result []*Dashboard
 	Result []*Dashboard
 }
 }
 
 
-type GetFoldersForSignedInUserQuery struct {
-	OrgId        int64
-	SignedInUser *SignedInUser
-	Title        string
-	Result       []*DashboardFolder
-}
-
-type DashboardFolder struct {
-	Id    int64  `json:"id"`
-	Title string `json:"title"`
-}
-
 type DashboardPermissionForUser struct {
 type DashboardPermissionForUser struct {
 	DashboardId    int64          `json:"dashboardId"`
 	DashboardId    int64          `json:"dashboardId"`
 	Permission     PermissionType `json:"permission"`
 	Permission     PermissionType `json:"permission"`

+ 16 - 15
pkg/models/datasource.go

@@ -58,21 +58,22 @@ type DataSource struct {
 }
 }
 
 
 var knownDatasourcePlugins map[string]bool = map[string]bool{
 var knownDatasourcePlugins map[string]bool = map[string]bool{
-	DS_ES:                                 true,
-	DS_GRAPHITE:                           true,
-	DS_INFLUXDB:                           true,
-	DS_INFLUXDB_08:                        true,
-	DS_KAIROSDB:                           true,
-	DS_CLOUDWATCH:                         true,
-	DS_PROMETHEUS:                         true,
-	DS_OPENTSDB:                           true,
-	DS_POSTGRES:                           true,
-	DS_MYSQL:                              true,
-	"opennms":                             true,
-	"druid":                               true,
-	"dalmatinerdb":                        true,
-	"gnocci":                              true,
-	"zabbix":                              true,
+	DS_ES:                       true,
+	DS_GRAPHITE:                 true,
+	DS_INFLUXDB:                 true,
+	DS_INFLUXDB_08:              true,
+	DS_KAIROSDB:                 true,
+	DS_CLOUDWATCH:               true,
+	DS_PROMETHEUS:               true,
+	DS_OPENTSDB:                 true,
+	DS_POSTGRES:                 true,
+	DS_MYSQL:                    true,
+	"opennms":                   true,
+	"abhisant-druid-datasource": true,
+	"dalmatinerdb-datasource":   true,
+	"gnocci":                    true,
+	"zabbix":                    true,
+	"alexanderzobnin-zabbix-datasource":   true,
 	"newrelic-app":                        true,
 	"newrelic-app":                        true,
 	"grafana-datadog-datasource":          true,
 	"grafana-datadog-datasource":          true,
 	"grafana-simple-json":                 true,
 	"grafana-simple-json":                 true,

+ 1 - 1
pkg/models/login_attempt.go

@@ -8,7 +8,7 @@ type LoginAttempt struct {
 	Id        int64
 	Id        int64
 	Username  string
 	Username  string
 	IpAddress string
 	IpAddress string
-	Created   time.Time
+	Created   int64
 }
 }
 
 
 // ---------------------
 // ---------------------

+ 3 - 2
pkg/models/team.go

@@ -7,8 +7,9 @@ import (
 
 
 // Typed errors
 // Typed errors
 var (
 var (
-	ErrTeamNotFound  = errors.New("Team not found")
-	ErrTeamNameTaken = errors.New("Team name is taken")
+	ErrTeamNotFound       = errors.New("Team not found")
+	ErrTeamNameTaken      = errors.New("Team name is taken")
+	ErrTeamMemberNotFound = errors.New("Team member not found")
 )
 )
 
 
 // Team model
 // Team model

+ 18 - 8
pkg/plugins/dashboard_importer.go

@@ -8,6 +8,7 @@ import (
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/services/dashboards"
 )
 )
 
 
 type ImportDashboardCommand struct {
 type ImportDashboardCommand struct {
@@ -17,7 +18,7 @@ type ImportDashboardCommand struct {
 	Overwrite bool
 	Overwrite bool
 
 
 	OrgId    int64
 	OrgId    int64
-	UserId   int64
+	User     *m.SignedInUser
 	PluginId string
 	PluginId string
 	Result   *PluginDashboardInfoDTO
 	Result   *PluginDashboardInfoDTO
 }
 }
@@ -34,7 +35,7 @@ type DashboardInputMissingError struct {
 }
 }
 
 
 func (e DashboardInputMissingError) Error() string {
 func (e DashboardInputMissingError) Error() string {
-	return fmt.Sprintf("Dashbord input variable: %v missing from import command", e.VariableName)
+	return fmt.Sprintf("Dashboard input variable: %v missing from import command", e.VariableName)
 }
 }
 
 
 func init() {
 func init() {
@@ -66,23 +67,32 @@ func ImportDashboard(cmd *ImportDashboardCommand) error {
 	saveCmd := m.SaveDashboardCommand{
 	saveCmd := m.SaveDashboardCommand{
 		Dashboard: generatedDash,
 		Dashboard: generatedDash,
 		OrgId:     cmd.OrgId,
 		OrgId:     cmd.OrgId,
-		UserId:    cmd.UserId,
+		UserId:    cmd.User.UserId,
 		Overwrite: cmd.Overwrite,
 		Overwrite: cmd.Overwrite,
 		PluginId:  cmd.PluginId,
 		PluginId:  cmd.PluginId,
 		FolderId:  dashboard.FolderId,
 		FolderId:  dashboard.FolderId,
 	}
 	}
 
 
-	if err := bus.Dispatch(&saveCmd); err != nil {
+	dto := &dashboards.SaveDashboardDTO{
+		OrgId:     cmd.OrgId,
+		Dashboard: saveCmd.GetDashboardModel(),
+		Overwrite: saveCmd.Overwrite,
+		User:      cmd.User,
+	}
+
+	savedDash, err := dashboards.NewService().SaveDashboard(dto)
+
+	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
 	cmd.Result = &PluginDashboardInfoDTO{
 	cmd.Result = &PluginDashboardInfoDTO{
 		PluginId:         cmd.PluginId,
 		PluginId:         cmd.PluginId,
-		Title:            dashboard.Title,
+		Title:            savedDash.Title,
 		Path:             cmd.Path,
 		Path:             cmd.Path,
-		Revision:         dashboard.Data.Get("revision").MustInt64(1),
-		ImportedUri:      "db/" + saveCmd.Result.Slug,
-		ImportedUrl:      saveCmd.Result.GetUrl(),
+		Revision:         savedDash.Data.Get("revision").MustInt64(1),
+		ImportedUri:      "db/" + savedDash.Slug,
+		ImportedUrl:      savedDash.GetUrl(),
 		ImportedRevision: dashboard.Data.Get("revision").MustInt64(1),
 		ImportedRevision: dashboard.Data.Get("revision").MustInt64(1),
 		Imported:         true,
 		Imported:         true,
 	}
 	}

+ 12 - 13
pkg/plugins/dashboard_importer_test.go

@@ -5,9 +5,9 @@ import (
 	"io/ioutil"
 	"io/ioutil"
 	"testing"
 	"testing"
 
 
-	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/services/dashboards"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
 	. "github.com/smartystreets/goconvey/convey"
 	. "github.com/smartystreets/goconvey/convey"
 	"gopkg.in/ini.v1"
 	"gopkg.in/ini.v1"
@@ -15,19 +15,15 @@ import (
 
 
 func TestDashboardImport(t *testing.T) {
 func TestDashboardImport(t *testing.T) {
 	pluginScenario("When importing a plugin dashboard", t, func() {
 	pluginScenario("When importing a plugin dashboard", t, func() {
-		var importedDash *m.Dashboard
-
-		bus.AddHandler("test", func(cmd *m.SaveDashboardCommand) error {
-			importedDash = cmd.GetDashboardModel()
-			cmd.Result = importedDash
-			return nil
-		})
+		origNewDashboardService := dashboards.NewService
+		mock := &dashboards.FakeDashboardService{}
+		dashboards.MockDashboardService(mock)
 
 
 		cmd := ImportDashboardCommand{
 		cmd := ImportDashboardCommand{
 			PluginId: "test-app",
 			PluginId: "test-app",
 			Path:     "dashboards/connections.json",
 			Path:     "dashboards/connections.json",
 			OrgId:    1,
 			OrgId:    1,
-			UserId:   1,
+			User:     &m.SignedInUser{UserId: 1, OrgRole: m.ROLE_ADMIN},
 			Inputs: []ImportDashboardInput{
 			Inputs: []ImportDashboardInput{
 				{Name: "*", Type: "datasource", Value: "graphite"},
 				{Name: "*", Type: "datasource", Value: "graphite"},
 			},
 			},
@@ -37,18 +33,22 @@ func TestDashboardImport(t *testing.T) {
 		So(err, ShouldBeNil)
 		So(err, ShouldBeNil)
 
 
 		Convey("should install dashboard", func() {
 		Convey("should install dashboard", func() {
-			So(importedDash, ShouldNotBeNil)
+			So(cmd.Result, ShouldNotBeNil)
 
 
-			resultStr, _ := importedDash.Data.EncodePretty()
+			resultStr, _ := mock.SavedDashboards[0].Dashboard.Data.EncodePretty()
 			expectedBytes, _ := ioutil.ReadFile("../../tests/test-app/dashboards/connections_result.json")
 			expectedBytes, _ := ioutil.ReadFile("../../tests/test-app/dashboards/connections_result.json")
 			expectedJson, _ := simplejson.NewJson(expectedBytes)
 			expectedJson, _ := simplejson.NewJson(expectedBytes)
 			expectedStr, _ := expectedJson.EncodePretty()
 			expectedStr, _ := expectedJson.EncodePretty()
 
 
 			So(string(resultStr), ShouldEqual, string(expectedStr))
 			So(string(resultStr), ShouldEqual, string(expectedStr))
 
 
-			panel := importedDash.Data.Get("rows").GetIndex(0).Get("panels").GetIndex(0)
+			panel := mock.SavedDashboards[0].Dashboard.Data.Get("rows").GetIndex(0).Get("panels").GetIndex(0)
 			So(panel.Get("datasource").MustString(), ShouldEqual, "graphite")
 			So(panel.Get("datasource").MustString(), ShouldEqual, "graphite")
 		})
 		})
+
+		Reset(func() {
+			dashboards.NewService = origNewDashboardService
+		})
 	})
 	})
 
 
 	Convey("When evaling dashboard template", t, func() {
 	Convey("When evaling dashboard template", t, func() {
@@ -84,7 +84,6 @@ func TestDashboardImport(t *testing.T) {
 		})
 		})
 
 
 	})
 	})
-
 }
 }
 
 
 func pluginScenario(desc string, t *testing.T, fn func()) {
 func pluginScenario(desc string, t *testing.T, fn func()) {

+ 1 - 1
pkg/plugins/dashboards_updater.go

@@ -47,7 +47,7 @@ func autoUpdateAppDashboard(pluginDashInfo *PluginDashboardInfoDTO, orgId int64)
 			PluginId:  pluginDashInfo.PluginId,
 			PluginId:  pluginDashInfo.PluginId,
 			Overwrite: true,
 			Overwrite: true,
 			Dashboard: dash.Data,
 			Dashboard: dash.Data,
-			UserId:    0,
+			User:      &m.SignedInUser{UserId: 0, OrgRole: m.ROLE_ADMIN},
 			Path:      pluginDashInfo.Path,
 			Path:      pluginDashInfo.Path,
 		}
 		}
 
 

+ 2 - 14
pkg/services/alerting/commands.go

@@ -5,24 +5,12 @@ import (
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
 )
 )
 
 
-type UpdateDashboardAlertsCommand struct {
-	UserId    int64
-	OrgId     int64
-	Dashboard *m.Dashboard
-}
-
-type ValidateDashboardAlertsCommand struct {
-	UserId    int64
-	OrgId     int64
-	Dashboard *m.Dashboard
-}
-
 func init() {
 func init() {
 	bus.AddHandler("alerting", updateDashboardAlerts)
 	bus.AddHandler("alerting", updateDashboardAlerts)
 	bus.AddHandler("alerting", validateDashboardAlerts)
 	bus.AddHandler("alerting", validateDashboardAlerts)
 }
 }
 
 
-func validateDashboardAlerts(cmd *ValidateDashboardAlertsCommand) error {
+func validateDashboardAlerts(cmd *m.ValidateDashboardAlertsCommand) error {
 	extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId)
 	extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId)
 
 
 	if _, err := extractor.GetAlerts(); err != nil {
 	if _, err := extractor.GetAlerts(); err != nil {
@@ -32,7 +20,7 @@ func validateDashboardAlerts(cmd *ValidateDashboardAlertsCommand) error {
 	return nil
 	return nil
 }
 }
 
 
-func updateDashboardAlerts(cmd *UpdateDashboardAlertsCommand) error {
+func updateDashboardAlerts(cmd *m.UpdateDashboardAlertsCommand) error {
 	saveAlerts := m.SaveAlertsCommand{
 	saveAlerts := m.SaveAlertsCommand{
 		OrgId:       cmd.OrgId,
 		OrgId:       cmd.OrgId,
 		UserId:      cmd.UserId,
 		UserId:      cmd.UserId,

+ 232 - 0
pkg/services/dashboards/dashboard_service.go

@@ -0,0 +1,232 @@
+package dashboards
+
+import (
+	"strings"
+	"time"
+
+	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/services/guardian"
+	"github.com/grafana/grafana/pkg/util"
+)
+
+// DashboardService service for operating on dashboards
+type DashboardService interface {
+	SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error)
+}
+
+// DashboardProvisioningService service for operating on provisioned dashboards
+type DashboardProvisioningService interface {
+	SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error)
+	SaveFolderForProvisionedDashboards(*SaveDashboardDTO) (*models.Dashboard, error)
+	GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error)
+}
+
+// NewService factory for creating a new dashboard service
+var NewService = func() DashboardService {
+	return &dashboardServiceImpl{}
+}
+
+// NewProvisioningService factory for creating a new dashboard provisioning service
+var NewProvisioningService = func() DashboardProvisioningService {
+	return &dashboardServiceImpl{}
+}
+
+type SaveDashboardDTO struct {
+	OrgId     int64
+	UpdatedAt time.Time
+	User      *models.SignedInUser
+	Message   string
+	Overwrite bool
+	Dashboard *models.Dashboard
+}
+
+type dashboardServiceImpl struct{}
+
+func (dr *dashboardServiceImpl) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) {
+	cmd := &models.GetProvisionedDashboardDataQuery{Name: name}
+	err := bus.Dispatch(cmd)
+	if err != nil {
+		return nil, err
+	}
+
+	return cmd.Result, nil
+}
+
+func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO) (*models.SaveDashboardCommand, error) {
+	dash := dto.Dashboard
+
+	dash.Title = strings.TrimSpace(dash.Title)
+	dash.Data.Set("title", dash.Title)
+	dash.SetUid(strings.TrimSpace(dash.Uid))
+
+	if dash.Title == "" {
+		return nil, models.ErrDashboardTitleEmpty
+	}
+
+	if dash.IsFolder && dash.FolderId > 0 {
+		return nil, models.ErrDashboardFolderCannotHaveParent
+	}
+
+	if dash.IsFolder && strings.ToLower(dash.Title) == strings.ToLower(models.RootFolderName) {
+		return nil, models.ErrDashboardFolderNameExists
+	}
+
+	if !util.IsValidShortUid(dash.Uid) {
+		return nil, models.ErrDashboardInvalidUid
+	} else if len(dash.Uid) > 40 {
+		return nil, models.ErrDashboardUidToLong
+	}
+
+	validateAlertsCmd := models.ValidateDashboardAlertsCommand{
+		OrgId:     dto.OrgId,
+		Dashboard: dash,
+	}
+
+	if err := bus.Dispatch(&validateAlertsCmd); err != nil {
+		return nil, models.ErrDashboardContainsInvalidAlertData
+	}
+
+	validateBeforeSaveCmd := models.ValidateDashboardBeforeSaveCommand{
+		OrgId:     dto.OrgId,
+		Dashboard: dash,
+		Overwrite: dto.Overwrite,
+	}
+
+	if err := bus.Dispatch(&validateBeforeSaveCmd); err != nil {
+		return nil, err
+	}
+
+	guard := guardian.New(dash.GetDashboardIdForSavePermissionCheck(), dto.OrgId, dto.User)
+	if canSave, err := guard.CanSave(); err != nil || !canSave {
+		if err != nil {
+			return nil, err
+		}
+		return nil, models.ErrDashboardUpdateAccessDenied
+	}
+
+	cmd := &models.SaveDashboardCommand{
+		Dashboard: dash.Data,
+		Message:   dto.Message,
+		OrgId:     dto.OrgId,
+		Overwrite: dto.Overwrite,
+		UserId:    dto.User.UserId,
+		FolderId:  dash.FolderId,
+		IsFolder:  dash.IsFolder,
+		PluginId:  dash.PluginId,
+	}
+
+	if !dto.UpdatedAt.IsZero() {
+		cmd.UpdatedAt = dto.UpdatedAt
+	}
+
+	return cmd, nil
+}
+
+func (dr *dashboardServiceImpl) updateAlerting(cmd *models.SaveDashboardCommand, dto *SaveDashboardDTO) error {
+	alertCmd := models.UpdateDashboardAlertsCommand{
+		OrgId:     dto.OrgId,
+		UserId:    dto.User.UserId,
+		Dashboard: cmd.Result,
+	}
+
+	if err := bus.Dispatch(&alertCmd); err != nil {
+		return models.ErrDashboardFailedToUpdateAlertData
+	}
+
+	return nil
+}
+
+func (dr *dashboardServiceImpl) SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) {
+	dto.User = &models.SignedInUser{
+		UserId:  0,
+		OrgRole: models.ROLE_ADMIN,
+	}
+	cmd, err := dr.buildSaveDashboardCommand(dto)
+	if err != nil {
+		return nil, err
+	}
+
+	saveCmd := &models.SaveProvisionedDashboardCommand{
+		DashboardCmd:          cmd,
+		DashboardProvisioning: provisioning,
+	}
+
+	// dashboard
+	err = bus.Dispatch(saveCmd)
+	if err != nil {
+		return nil, err
+	}
+
+	//alerts
+	err = dr.updateAlerting(cmd, dto)
+	if err != nil {
+		return nil, err
+	}
+
+	return cmd.Result, nil
+}
+
+func (dr *dashboardServiceImpl) SaveFolderForProvisionedDashboards(dto *SaveDashboardDTO) (*models.Dashboard, error) {
+	dto.User = &models.SignedInUser{
+		UserId:  0,
+		OrgRole: models.ROLE_ADMIN,
+	}
+	cmd, err := dr.buildSaveDashboardCommand(dto)
+	if err != nil {
+		return nil, err
+	}
+
+	err = bus.Dispatch(cmd)
+	if err != nil {
+		return nil, err
+	}
+
+	err = dr.updateAlerting(cmd, dto)
+	if err != nil {
+		return nil, err
+	}
+
+	return cmd.Result, nil
+}
+
+func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
+	cmd, err := dr.buildSaveDashboardCommand(dto)
+	if err != nil {
+		return nil, err
+	}
+
+	err = bus.Dispatch(cmd)
+	if err != nil {
+		return nil, err
+	}
+
+	err = dr.updateAlerting(cmd, dto)
+	if err != nil {
+		return nil, err
+	}
+
+	return cmd.Result, nil
+}
+
+type FakeDashboardService struct {
+	SaveDashboardResult *models.Dashboard
+	SaveDashboardError  error
+	SavedDashboards     []*SaveDashboardDTO
+}
+
+func (s *FakeDashboardService) SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
+	s.SavedDashboards = append(s.SavedDashboards, dto)
+
+	if s.SaveDashboardResult == nil && s.SaveDashboardError == nil {
+		s.SaveDashboardResult = dto.Dashboard
+	}
+
+	return s.SaveDashboardResult, s.SaveDashboardError
+}
+
+func MockDashboardService(mock *FakeDashboardService) {
+	NewService = func() DashboardService {
+		return mock
+	}
+}

+ 144 - 0
pkg/services/dashboards/dashboard_service_test.go

@@ -0,0 +1,144 @@
+package dashboards
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/grafana/grafana/pkg/services/guardian"
+
+	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/models"
+
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestDashboardService(t *testing.T) {
+	Convey("Dashboard service tests", t, func() {
+		service := dashboardServiceImpl{}
+
+		origNewDashboardGuardian := guardian.New
+		mockDashboardGuardian(&fakeDashboardGuardian{canSave: true})
+
+		Convey("Save dashboard validation", func() {
+			dto := &SaveDashboardDTO{}
+
+			Convey("When saving a dashboard with empty title it should return error", func() {
+				titles := []string{"", " ", "   \t   "}
+
+				for _, title := range titles {
+					dto.Dashboard = models.NewDashboard(title)
+					_, err := service.SaveDashboard(dto)
+					So(err, ShouldEqual, models.ErrDashboardTitleEmpty)
+				}
+			})
+
+			Convey("Should return validation error if it's a folder and have a folder id", func() {
+				dto.Dashboard = models.NewDashboardFolder("Folder")
+				dto.Dashboard.FolderId = 1
+				_, err := service.SaveDashboard(dto)
+				So(err, ShouldEqual, models.ErrDashboardFolderCannotHaveParent)
+			})
+
+			Convey("Should return validation error if folder is named General", func() {
+				dto.Dashboard = models.NewDashboardFolder("General")
+				_, err := service.SaveDashboard(dto)
+				So(err, ShouldEqual, models.ErrDashboardFolderNameExists)
+			})
+
+			Convey("When saving a dashboard should validate uid", func() {
+				bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
+					return nil
+				})
+
+				bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
+					return nil
+				})
+
+				testCases := []struct {
+					Uid   string
+					Error error
+				}{
+					{Uid: "", Error: nil},
+					{Uid: "   ", Error: nil},
+					{Uid: "  \t  ", Error: nil},
+					{Uid: "asdf90_-", Error: nil},
+					{Uid: "asdf/90", Error: models.ErrDashboardInvalidUid},
+					{Uid: "   asdfghjklqwertyuiopzxcvbnmasdfghjklqwer   ", Error: nil},
+					{Uid: "asdfghjklqwertyuiopzxcvbnmasdfghjklqwertyuiopzxcvbnmasdfghjklqwertyuiopzxcvbnm", Error: models.ErrDashboardUidToLong},
+				}
+
+				for _, tc := range testCases {
+					dto.Dashboard = models.NewDashboard("title")
+					dto.Dashboard.SetUid(tc.Uid)
+					dto.User = &models.SignedInUser{}
+
+					_, err := service.buildSaveDashboardCommand(dto)
+					So(err, ShouldEqual, tc.Error)
+				}
+			})
+
+			Convey("Should return validation error if alert data is invalid", func() {
+				bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
+					return errors.New("error")
+				})
+
+				dto.Dashboard = models.NewDashboard("Dash")
+				_, err := service.SaveDashboard(dto)
+				So(err, ShouldEqual, models.ErrDashboardContainsInvalidAlertData)
+			})
+		})
+
+		Reset(func() {
+			guardian.New = origNewDashboardGuardian
+		})
+	})
+}
+
+func mockDashboardGuardian(mock *fakeDashboardGuardian) {
+	guardian.New = func(dashId int64, orgId int64, user *models.SignedInUser) guardian.DashboardGuardian {
+		mock.orgId = orgId
+		mock.dashId = dashId
+		mock.user = user
+		return mock
+	}
+}
+
+type fakeDashboardGuardian struct {
+	dashId                      int64
+	orgId                       int64
+	user                        *models.SignedInUser
+	canSave                     bool
+	canEdit                     bool
+	canView                     bool
+	canAdmin                    bool
+	hasPermission               bool
+	checkPermissionBeforeUpdate bool
+}
+
+func (g *fakeDashboardGuardian) CanSave() (bool, error) {
+	return g.canSave, nil
+}
+
+func (g *fakeDashboardGuardian) CanEdit() (bool, error) {
+	return g.canEdit, nil
+}
+
+func (g *fakeDashboardGuardian) CanView() (bool, error) {
+	return g.canView, nil
+}
+
+func (g *fakeDashboardGuardian) CanAdmin() (bool, error) {
+	return g.canAdmin, nil
+}
+
+func (g *fakeDashboardGuardian) HasPermission(permission models.PermissionType) (bool, error) {
+	return g.hasPermission, nil
+}
+
+func (g *fakeDashboardGuardian) CheckPermissionBeforeUpdate(permission models.PermissionType, updatePermissions []*models.DashboardAcl) (bool, error) {
+	return g.checkPermissionBeforeUpdate, nil
+}
+
+func (g *fakeDashboardGuardian) GetAcl() ([]*models.DashboardAclInfoDTO, error) {
+	return nil, nil
+}

+ 0 - 82
pkg/services/dashboards/dashboards.go

@@ -1,82 +0,0 @@
-package dashboards
-
-import (
-	"time"
-
-	"github.com/grafana/grafana/pkg/bus"
-	"github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/services/alerting"
-)
-
-type Repository interface {
-	SaveDashboard(*SaveDashboardItem) (*models.Dashboard, error)
-}
-
-var repositoryInstance Repository
-
-func GetRepository() Repository {
-	return repositoryInstance
-}
-
-func SetRepository(rep Repository) {
-	repositoryInstance = rep
-}
-
-type SaveDashboardItem struct {
-	OrgId     int64
-	UpdatedAt time.Time
-	UserId    int64
-	Message   string
-	Overwrite bool
-	Dashboard *models.Dashboard
-}
-
-type DashboardRepository struct{}
-
-func (dr *DashboardRepository) SaveDashboard(json *SaveDashboardItem) (*models.Dashboard, error) {
-	dashboard := json.Dashboard
-
-	if dashboard.Title == "" {
-		return nil, models.ErrDashboardTitleEmpty
-	}
-
-	validateAlertsCmd := alerting.ValidateDashboardAlertsCommand{
-		OrgId:     json.OrgId,
-		Dashboard: dashboard,
-	}
-
-	if err := bus.Dispatch(&validateAlertsCmd); err != nil {
-		return nil, models.ErrDashboardContainsInvalidAlertData
-	}
-
-	cmd := models.SaveDashboardCommand{
-		Dashboard: dashboard.Data,
-		Message:   json.Message,
-		OrgId:     json.OrgId,
-		Overwrite: json.Overwrite,
-		UserId:    json.UserId,
-		FolderId:  dashboard.FolderId,
-		IsFolder:  dashboard.IsFolder,
-	}
-
-	if !json.UpdatedAt.IsZero() {
-		cmd.UpdatedAt = json.UpdatedAt
-	}
-
-	err := bus.Dispatch(&cmd)
-	if err != nil {
-		return nil, err
-	}
-
-	alertCmd := alerting.UpdateDashboardAlertsCommand{
-		OrgId:     json.OrgId,
-		UserId:    json.UserId,
-		Dashboard: cmd.Result,
-	}
-
-	if err := bus.Dispatch(&alertCmd); err != nil {
-		return nil, models.ErrDashboardFailedToUpdateAlertData
-	}
-
-	return cmd.Result, nil
-}

+ 24 - 32
pkg/services/guardian/guardian.go

@@ -7,7 +7,18 @@ import (
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
 )
 )
 
 
-type DashboardGuardian struct {
+// DashboardGuardian to be used for guard against operations without access on dashboard and acl
+type DashboardGuardian interface {
+	CanSave() (bool, error)
+	CanEdit() (bool, error)
+	CanView() (bool, error)
+	CanAdmin() (bool, error)
+	HasPermission(permission m.PermissionType) (bool, error)
+	CheckPermissionBeforeUpdate(permission m.PermissionType, updatePermissions []*m.DashboardAcl) (bool, error)
+	GetAcl() ([]*m.DashboardAclInfoDTO, error)
+}
+
+type dashboardGuardianImpl struct {
 	user   *m.SignedInUser
 	user   *m.SignedInUser
 	dashId int64
 	dashId int64
 	orgId  int64
 	orgId  int64
@@ -16,8 +27,9 @@ type DashboardGuardian struct {
 	log    log.Logger
 	log    log.Logger
 }
 }
 
 
-func NewDashboardGuardian(dashId int64, orgId int64, user *m.SignedInUser) *DashboardGuardian {
-	return &DashboardGuardian{
+// New factory for creating a new dashboard guardian instance
+var New = func(dashId int64, orgId int64, user *m.SignedInUser) DashboardGuardian {
+	return &dashboardGuardianImpl{
 		user:   user,
 		user:   user,
 		dashId: dashId,
 		dashId: dashId,
 		orgId:  orgId,
 		orgId:  orgId,
@@ -25,11 +37,11 @@ func NewDashboardGuardian(dashId int64, orgId int64, user *m.SignedInUser) *Dash
 	}
 	}
 }
 }
 
 
-func (g *DashboardGuardian) CanSave() (bool, error) {
+func (g *dashboardGuardianImpl) CanSave() (bool, error) {
 	return g.HasPermission(m.PERMISSION_EDIT)
 	return g.HasPermission(m.PERMISSION_EDIT)
 }
 }
 
 
-func (g *DashboardGuardian) CanEdit() (bool, error) {
+func (g *dashboardGuardianImpl) CanEdit() (bool, error) {
 	if setting.ViewersCanEdit {
 	if setting.ViewersCanEdit {
 		return g.HasPermission(m.PERMISSION_VIEW)
 		return g.HasPermission(m.PERMISSION_VIEW)
 	}
 	}
@@ -37,15 +49,15 @@ func (g *DashboardGuardian) CanEdit() (bool, error) {
 	return g.HasPermission(m.PERMISSION_EDIT)
 	return g.HasPermission(m.PERMISSION_EDIT)
 }
 }
 
 
-func (g *DashboardGuardian) CanView() (bool, error) {
+func (g *dashboardGuardianImpl) CanView() (bool, error) {
 	return g.HasPermission(m.PERMISSION_VIEW)
 	return g.HasPermission(m.PERMISSION_VIEW)
 }
 }
 
 
-func (g *DashboardGuardian) CanAdmin() (bool, error) {
+func (g *dashboardGuardianImpl) CanAdmin() (bool, error) {
 	return g.HasPermission(m.PERMISSION_ADMIN)
 	return g.HasPermission(m.PERMISSION_ADMIN)
 }
 }
 
 
-func (g *DashboardGuardian) HasPermission(permission m.PermissionType) (bool, error) {
+func (g *dashboardGuardianImpl) HasPermission(permission m.PermissionType) (bool, error) {
 	if g.user.OrgRole == m.ROLE_ADMIN {
 	if g.user.OrgRole == m.ROLE_ADMIN {
 		return true, nil
 		return true, nil
 	}
 	}
@@ -58,7 +70,7 @@ func (g *DashboardGuardian) HasPermission(permission m.PermissionType) (bool, er
 	return g.checkAcl(permission, acl)
 	return g.checkAcl(permission, acl)
 }
 }
 
 
-func (g *DashboardGuardian) checkAcl(permission m.PermissionType, acl []*m.DashboardAclInfoDTO) (bool, error) {
+func (g *dashboardGuardianImpl) checkAcl(permission m.PermissionType, acl []*m.DashboardAclInfoDTO) (bool, error) {
 	orgRole := g.user.OrgRole
 	orgRole := g.user.OrgRole
 	teamAclItems := []*m.DashboardAclInfoDTO{}
 	teamAclItems := []*m.DashboardAclInfoDTO{}
 
 
@@ -106,27 +118,7 @@ func (g *DashboardGuardian) checkAcl(permission m.PermissionType, acl []*m.Dashb
 	return false, nil
 	return false, nil
 }
 }
 
 
-func (g *DashboardGuardian) CheckPermissionBeforeRemove(permission m.PermissionType, aclIdToRemove int64) (bool, error) {
-	if g.user.OrgRole == m.ROLE_ADMIN {
-		return true, nil
-	}
-
-	acl, err := g.GetAcl()
-	if err != nil {
-		return false, err
-	}
-
-	for i, p := range acl {
-		if p.Id == aclIdToRemove {
-			acl = append(acl[:i], acl[i+1:]...)
-			break
-		}
-	}
-
-	return g.checkAcl(permission, acl)
-}
-
-func (g *DashboardGuardian) CheckPermissionBeforeUpdate(permission m.PermissionType, updatePermissions []*m.DashboardAcl) (bool, error) {
+func (g *dashboardGuardianImpl) CheckPermissionBeforeUpdate(permission m.PermissionType, updatePermissions []*m.DashboardAcl) (bool, error) {
 	if g.user.OrgRole == m.ROLE_ADMIN {
 	if g.user.OrgRole == m.ROLE_ADMIN {
 		return true, nil
 		return true, nil
 	}
 	}
@@ -141,7 +133,7 @@ func (g *DashboardGuardian) CheckPermissionBeforeUpdate(permission m.PermissionT
 }
 }
 
 
 // GetAcl returns dashboard acl
 // GetAcl returns dashboard acl
-func (g *DashboardGuardian) GetAcl() ([]*m.DashboardAclInfoDTO, error) {
+func (g *dashboardGuardianImpl) GetAcl() ([]*m.DashboardAclInfoDTO, error) {
 	if g.acl != nil {
 	if g.acl != nil {
 		return g.acl, nil
 		return g.acl, nil
 	}
 	}
@@ -155,7 +147,7 @@ func (g *DashboardGuardian) GetAcl() ([]*m.DashboardAclInfoDTO, error) {
 	return g.acl, nil
 	return g.acl, nil
 }
 }
 
 
-func (g *DashboardGuardian) getTeams() ([]*m.Team, error) {
+func (g *dashboardGuardianImpl) getTeams() ([]*m.Team, error) {
 	if g.groups != nil {
 	if g.groups != nil {
 		return g.groups, nil
 		return g.groups, nil
 	}
 	}

+ 43 - 10
pkg/services/provisioning/dashboards/config_reader.go

@@ -2,6 +2,7 @@ package dashboards
 
 
 import (
 import (
 	"io/ioutil"
 	"io/ioutil"
+	"os"
 	"path/filepath"
 	"path/filepath"
 	"strings"
 	"strings"
 
 
@@ -14,11 +15,48 @@ type configReader struct {
 	log  log.Logger
 	log  log.Logger
 }
 }
 
 
+func (cr *configReader) parseConfigs(file os.FileInfo) ([]*DashboardsAsConfig, error) {
+	filename, _ := filepath.Abs(filepath.Join(cr.path, file.Name()))
+	yamlFile, err := ioutil.ReadFile(filename)
+	if err != nil {
+		return nil, err
+	}
+
+	apiVersion := &ConfigVersion{ApiVersion: 0}
+	yaml.Unmarshal(yamlFile, &apiVersion)
+
+	if apiVersion.ApiVersion > 0 {
+
+		v1 := &DashboardAsConfigV1{}
+		err := yaml.Unmarshal(yamlFile, &v1)
+		if err != nil {
+			return nil, err
+		}
+
+		if v1 != nil {
+			return v1.mapToDashboardAsConfig(), nil
+		}
+
+	} else {
+		var v0 []*DashboardsAsConfigV0
+		err := yaml.Unmarshal(yamlFile, &v0)
+		if err != nil {
+			return nil, err
+		}
+
+		if v0 != nil {
+			cr.log.Warn("[Deprecated] the dashboard provisioning config is outdated. please upgrade", "filename", filename)
+			return mapV0ToDashboardAsConfig(v0), nil
+		}
+	}
+
+	return []*DashboardsAsConfig{}, nil
+}
+
 func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) {
 func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) {
 	var dashboards []*DashboardsAsConfig
 	var dashboards []*DashboardsAsConfig
 
 
 	files, err := ioutil.ReadDir(cr.path)
 	files, err := ioutil.ReadDir(cr.path)
-
 	if err != nil {
 	if err != nil {
 		cr.log.Error("cant read dashboard provisioning files from directory", "path", cr.path)
 		cr.log.Error("cant read dashboard provisioning files from directory", "path", cr.path)
 		return dashboards, nil
 		return dashboards, nil
@@ -29,19 +67,14 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) {
 			continue
 			continue
 		}
 		}
 
 
-		filename, _ := filepath.Abs(filepath.Join(cr.path, file.Name()))
-		yamlFile, err := ioutil.ReadFile(filename)
+		parsedDashboards, err := cr.parseConfigs(file)
 		if err != nil {
 		if err != nil {
-			return nil, err
-		}
 
 
-		var dashCfg []*DashboardsAsConfig
-		err = yaml.Unmarshal(yamlFile, &dashCfg)
-		if err != nil {
-			return nil, err
 		}
 		}
 
 
-		dashboards = append(dashboards, dashCfg...)
+		if len(parsedDashboards) > 0 {
+			dashboards = append(dashboards, parsedDashboards...)
+		}
 	}
 	}
 
 
 	for i := range dashboards {
 	for i := range dashboards {

+ 37 - 29
pkg/services/provisioning/dashboards/config_reader_test.go

@@ -9,48 +9,33 @@ import (
 
 
 var (
 var (
 	simpleDashboardConfig string = "./test-configs/dashboards-from-disk"
 	simpleDashboardConfig string = "./test-configs/dashboards-from-disk"
+	oldVersion            string = "./test-configs/version-0"
 	brokenConfigs         string = "./test-configs/broken-configs"
 	brokenConfigs         string = "./test-configs/broken-configs"
 )
 )
 
 
 func TestDashboardsAsConfig(t *testing.T) {
 func TestDashboardsAsConfig(t *testing.T) {
 	Convey("Dashboards as configuration", t, func() {
 	Convey("Dashboards as configuration", t, func() {
+		logger := log.New("test-logger")
 
 
-		Convey("Can read config file", func() {
-
-			cfgProvider := configReader{path: simpleDashboardConfig, log: log.New("test-logger")}
+		Convey("Can read config file version 1 format", func() {
+			cfgProvider := configReader{path: simpleDashboardConfig, log: logger}
 			cfg, err := cfgProvider.readConfig()
 			cfg, err := cfgProvider.readConfig()
-			if err != nil {
-				t.Fatalf("readConfig return an error %v", err)
-			}
-
-			So(len(cfg), ShouldEqual, 2)
-
-			ds := cfg[0]
+			So(err, ShouldBeNil)
 
 
-			So(ds.Name, ShouldEqual, "general dashboards")
-			So(ds.Type, ShouldEqual, "file")
-			So(ds.OrgId, ShouldEqual, 2)
-			So(ds.Folder, ShouldEqual, "developers")
-			So(ds.Editable, ShouldBeTrue)
-
-			So(len(ds.Options), ShouldEqual, 1)
-			So(ds.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards")
-
-			ds2 := cfg[1]
+			validateDashboardAsConfig(cfg)
+		})
 
 
-			So(ds2.Name, ShouldEqual, "default")
-			So(ds2.Type, ShouldEqual, "file")
-			So(ds2.OrgId, ShouldEqual, 1)
-			So(ds2.Folder, ShouldEqual, "")
-			So(ds2.Editable, ShouldBeFalse)
+		Convey("Can read config file in version 0 format", func() {
+			cfgProvider := configReader{path: oldVersion, log: logger}
+			cfg, err := cfgProvider.readConfig()
+			So(err, ShouldBeNil)
 
 
-			So(len(ds2.Options), ShouldEqual, 1)
-			So(ds2.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards")
+			validateDashboardAsConfig(cfg)
 		})
 		})
 
 
 		Convey("Should skip invalid path", func() {
 		Convey("Should skip invalid path", func() {
 
 
-			cfgProvider := configReader{path: "/invalid-directory", log: log.New("test-logger")}
+			cfgProvider := configReader{path: "/invalid-directory", log: logger}
 			cfg, err := cfgProvider.readConfig()
 			cfg, err := cfgProvider.readConfig()
 			if err != nil {
 			if err != nil {
 				t.Fatalf("readConfig return an error %v", err)
 				t.Fatalf("readConfig return an error %v", err)
@@ -61,7 +46,7 @@ func TestDashboardsAsConfig(t *testing.T) {
 
 
 		Convey("Should skip broken config files", func() {
 		Convey("Should skip broken config files", func() {
 
 
-			cfgProvider := configReader{path: brokenConfigs, log: log.New("test-logger")}
+			cfgProvider := configReader{path: brokenConfigs, log: logger}
 			cfg, err := cfgProvider.readConfig()
 			cfg, err := cfgProvider.readConfig()
 			if err != nil {
 			if err != nil {
 				t.Fatalf("readConfig return an error %v", err)
 				t.Fatalf("readConfig return an error %v", err)
@@ -71,3 +56,26 @@ func TestDashboardsAsConfig(t *testing.T) {
 		})
 		})
 	})
 	})
 }
 }
+func validateDashboardAsConfig(cfg []*DashboardsAsConfig) {
+	So(len(cfg), ShouldEqual, 2)
+
+	ds := cfg[0]
+	So(ds.Name, ShouldEqual, "general dashboards")
+	So(ds.Type, ShouldEqual, "file")
+	So(ds.OrgId, ShouldEqual, 2)
+	So(ds.Folder, ShouldEqual, "developers")
+	So(ds.Editable, ShouldBeTrue)
+	So(len(ds.Options), ShouldEqual, 1)
+	So(ds.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards")
+	So(ds.DisableDeletion, ShouldBeTrue)
+
+	ds2 := cfg[1]
+	So(ds2.Name, ShouldEqual, "default")
+	So(ds2.Type, ShouldEqual, "file")
+	So(ds2.OrgId, ShouldEqual, 1)
+	So(ds2.Folder, ShouldEqual, "")
+	So(ds2.Editable, ShouldBeFalse)
+	So(len(ds2.Options), ShouldEqual, 1)
+	So(ds2.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards")
+	So(ds2.DisableDeletion, ShouldBeFalse)
+}

+ 0 - 33
pkg/services/provisioning/dashboards/dashboard_cache.go

@@ -1,33 +0,0 @@
-package dashboards
-
-import (
-	"github.com/grafana/grafana/pkg/services/dashboards"
-	gocache "github.com/patrickmn/go-cache"
-	"time"
-)
-
-type dashboardCache struct {
-	internalCache *gocache.Cache
-}
-
-func NewDashboardCache() *dashboardCache {
-	return &dashboardCache{internalCache: gocache.New(5*time.Minute, 30*time.Minute)}
-}
-
-func (fr *dashboardCache) addDashboardCache(key string, json *dashboards.SaveDashboardItem) {
-	fr.internalCache.Add(key, json, time.Minute*10)
-}
-
-func (fr *dashboardCache) getCache(key string) (*dashboards.SaveDashboardItem, bool) {
-	obj, exist := fr.internalCache.Get(key)
-	if !exist {
-		return nil, exist
-	}
-
-	dash, ok := obj.(*dashboards.SaveDashboardItem)
-	if !ok {
-		return nil, ok
-	}
-
-	return dash, ok
-}

+ 191 - 82
pkg/services/provisioning/dashboards/file_reader.go

@@ -25,12 +25,10 @@ var (
 )
 )
 
 
 type fileReader struct {
 type fileReader struct {
-	Cfg           *DashboardsAsConfig
-	Path          string
-	log           log.Logger
-	dashboardRepo dashboards.Repository
-	cache         *dashboardCache
-	createWalk    func(fr *fileReader, folderId int64) filepath.WalkFunc
+	Cfg              *DashboardsAsConfig
+	Path             string
+	log              log.Logger
+	dashboardService dashboards.DashboardProvisioningService
 }
 }
 
 
 func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReader, error) {
 func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReader, error) {
@@ -50,28 +48,26 @@ func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReade
 	}
 	}
 
 
 	return &fileReader{
 	return &fileReader{
-		Cfg:           cfg,
-		Path:          path,
-		log:           log,
-		dashboardRepo: dashboards.GetRepository(),
-		cache:         NewDashboardCache(),
-		createWalk:    createWalkFn,
+		Cfg:              cfg,
+		Path:             path,
+		log:              log,
+		dashboardService: dashboards.NewProvisioningService(),
 	}, nil
 	}, nil
 }
 }
 
 
 func (fr *fileReader) ReadAndListen(ctx context.Context) error {
 func (fr *fileReader) ReadAndListen(ctx context.Context) error {
-	ticker := time.NewTicker(checkDiskForChangesInterval)
-
 	if err := fr.startWalkingDisk(); err != nil {
 	if err := fr.startWalkingDisk(); err != nil {
 		fr.log.Error("failed to search for dashboards", "error", err)
 		fr.log.Error("failed to search for dashboards", "error", err)
 	}
 	}
 
 
+	ticker := time.NewTicker(checkDiskForChangesInterval)
+
 	running := false
 	running := false
 
 
 	for {
 	for {
 		select {
 		select {
 		case <-ticker.C:
 		case <-ticker.C:
-			if !running { // avoid walking the filesystem in parallel. incase fs is very slow.
+			if !running { // avoid walking the filesystem in parallel. in-case fs is very slow.
 				running = true
 				running = true
 				go func() {
 				go func() {
 					if err := fr.startWalkingDisk(); err != nil {
 					if err := fr.startWalkingDisk(); err != nil {
@@ -93,15 +89,116 @@ func (fr *fileReader) startWalkingDisk() error {
 		}
 		}
 	}
 	}
 
 
-	folderId, err := getOrCreateFolderId(fr.Cfg, fr.dashboardRepo)
+	folderId, err := getOrCreateFolderId(fr.Cfg, fr.dashboardService)
 	if err != nil && err != ErrFolderNameMissing {
 	if err != nil && err != ErrFolderNameMissing {
 		return err
 		return err
 	}
 	}
 
 
-	return filepath.Walk(fr.Path, fr.createWalk(fr, folderId))
+	provisionedDashboardRefs, err := getProvisionedDashboardByPath(fr.dashboardService, fr.Cfg.Name)
+	if err != nil {
+		return err
+	}
+
+	filesFoundOnDisk := map[string]os.FileInfo{}
+	err = filepath.Walk(fr.Path, createWalkFn(filesFoundOnDisk))
+	if err != nil {
+		return err
+	}
+
+	fr.deleteDashboardIfFileIsMissing(provisionedDashboardRefs, filesFoundOnDisk)
+
+	sanityChecker := newProvisioningSanityChecker(fr.Cfg.Name)
+
+	// save dashboards based on json files
+	for path, fileInfo := range filesFoundOnDisk {
+		provisioningMetadata, err := fr.saveDashboard(path, folderId, fileInfo, provisionedDashboardRefs)
+		sanityChecker.track(provisioningMetadata)
+		if err != nil {
+			fr.log.Error("failed to save dashboard", "error", err)
+		}
+	}
+	sanityChecker.logWarnings(fr.log)
+
+	return nil
+}
+func (fr *fileReader) deleteDashboardIfFileIsMissing(provisionedDashboardRefs map[string]*models.DashboardProvisioning, filesFoundOnDisk map[string]os.FileInfo) {
+	if fr.Cfg.DisableDeletion {
+		return
+	}
+
+	// find dashboards to delete since json file is missing
+	var dashboardToDelete []int64
+	for path, provisioningData := range provisionedDashboardRefs {
+		_, existsOnDisk := filesFoundOnDisk[path]
+		if !existsOnDisk {
+			dashboardToDelete = append(dashboardToDelete, provisioningData.DashboardId)
+		}
+	}
+	// delete dashboard that are missing json file
+	for _, dashboardId := range dashboardToDelete {
+		fr.log.Debug("deleting provisioned dashboard. missing on disk", "id", dashboardId)
+		cmd := &models.DeleteDashboardCommand{OrgId: fr.Cfg.OrgId, Id: dashboardId}
+		err := bus.Dispatch(cmd)
+		if err != nil {
+			fr.log.Error("failed to delete dashboard", "id", cmd.Id)
+		}
+	}
 }
 }
 
 
-func getOrCreateFolderId(cfg *DashboardsAsConfig, repo dashboards.Repository) (int64, error) {
+func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.FileInfo, provisionedDashboardRefs map[string]*models.DashboardProvisioning) (provisioningMetadata, error) {
+	provisioningMetadata := provisioningMetadata{}
+	resolvedFileInfo, err := resolveSymlink(fileInfo, path)
+	if err != nil {
+		return provisioningMetadata, err
+	}
+
+	provisionedData, alreadyProvisioned := provisionedDashboardRefs[path]
+	upToDate := alreadyProvisioned && provisionedData.Updated == resolvedFileInfo.ModTime().Unix()
+
+	dash, err := fr.readDashboardFromFile(path, resolvedFileInfo.ModTime(), folderId)
+	if err != nil {
+		fr.log.Error("failed to load dashboard from ", "file", path, "error", err)
+		return provisioningMetadata, nil
+	}
+
+	// keeps track of what uid's and title's we have already provisioned
+	provisioningMetadata.uid = dash.Dashboard.Uid
+	provisioningMetadata.title = dash.Dashboard.Title
+
+	if upToDate {
+		return provisioningMetadata, nil
+	}
+
+	if dash.Dashboard.Id != 0 {
+		fr.log.Error("provisioned dashboard json files cannot contain id")
+		return provisioningMetadata, nil
+	}
+
+	if alreadyProvisioned {
+		dash.Dashboard.SetId(provisionedData.DashboardId)
+	}
+
+	fr.log.Debug("saving new dashboard", "file", path)
+	dp := &models.DashboardProvisioning{ExternalId: path, Name: fr.Cfg.Name, Updated: resolvedFileInfo.ModTime().Unix()}
+	_, err = fr.dashboardService.SaveProvisionedDashboard(dash, dp)
+	return provisioningMetadata, err
+}
+
+func getProvisionedDashboardByPath(service dashboards.DashboardProvisioningService, name string) (map[string]*models.DashboardProvisioning, error) {
+	arr, err := service.GetProvisionedDashboardData(name)
+	if err != nil {
+		return nil, err
+	}
+
+	byPath := map[string]*models.DashboardProvisioning{}
+	for _, pd := range arr {
+		byPath[pd.ExternalId] = pd
+	}
+
+	return byPath, nil
+}
+
+func getOrCreateFolderId(cfg *DashboardsAsConfig, service dashboards.DashboardProvisioningService) (int64, error) {
 	if cfg.Folder == "" {
 	if cfg.Folder == "" {
 		return 0, ErrFolderNameMissing
 		return 0, ErrFolderNameMissing
 	}
 	}
@@ -115,12 +212,12 @@ func getOrCreateFolderId(cfg *DashboardsAsConfig, repo dashboards.Repository) (i
 
 
 	// dashboard folder not found. create one.
 	// dashboard folder not found. create one.
 	if err == models.ErrDashboardNotFound {
 	if err == models.ErrDashboardNotFound {
-		dash := &dashboards.SaveDashboardItem{}
-		dash.Dashboard = models.NewDashboard(cfg.Folder)
+		dash := &dashboards.SaveDashboardDTO{}
+		dash.Dashboard = models.NewDashboardFolder(cfg.Folder)
 		dash.Dashboard.IsFolder = true
 		dash.Dashboard.IsFolder = true
 		dash.Overwrite = true
 		dash.Overwrite = true
 		dash.OrgId = cfg.OrgId
 		dash.OrgId = cfg.OrgId
-		dbDash, err := repo.SaveDashboard(dash)
+		dbDash, err := service.SaveFolderForProvisionedDashboards(dash)
 		if err != nil {
 		if err != nil {
 			return 0, err
 			return 0, err
 		}
 		}
@@ -129,83 +226,59 @@ func getOrCreateFolderId(cfg *DashboardsAsConfig, repo dashboards.Repository) (i
 	}
 	}
 
 
 	if !cmd.Result.IsFolder {
 	if !cmd.Result.IsFolder {
-		return 0, fmt.Errorf("Got invalid response. Expected folder, found dashboard")
+		return 0, fmt.Errorf("got invalid response. expected folder, found dashboard")
 	}
 	}
 
 
 	return cmd.Result.Id, nil
 	return cmd.Result.Id, nil
 }
 }
 
 
-func createWalkFn(fr *fileReader, folderId int64) filepath.WalkFunc {
-	return func(path string, fileInfo os.FileInfo, err error) error {
+func resolveSymlink(fileinfo os.FileInfo, path string) (os.FileInfo, error) {
+	checkFilepath, err := filepath.EvalSymlinks(path)
+	if path != checkFilepath {
+		path = checkFilepath
+		fi, err := os.Lstat(checkFilepath)
 		if err != nil {
 		if err != nil {
-			return err
-		}
-		if fileInfo.IsDir() {
-			if strings.HasPrefix(fileInfo.Name(), ".") {
-				return filepath.SkipDir
-			}
-			return nil
-		}
-
-		if !strings.HasSuffix(fileInfo.Name(), ".json") {
-			return nil
+			return nil, err
 		}
 		}
 
 
-		checkFilepath, err := filepath.EvalSymlinks(path)
-
-		if path != checkFilepath {
-			path = checkFilepath
-			fi, err := os.Lstat(checkFilepath)
-			if err != nil {
-				return err
-			}
-			fileInfo = fi
-		}
+		return fi, nil
+	}
 
 
-		cachedDashboard, exist := fr.cache.getCache(path)
-		if exist && cachedDashboard.UpdatedAt == fileInfo.ModTime() {
-			return nil
-		}
+	return fileinfo, err
+}
 
 
-		dash, err := fr.readDashboardFromFile(path, folderId)
+func createWalkFn(filesOnDisk map[string]os.FileInfo) filepath.WalkFunc {
+	return func(path string, fileInfo os.FileInfo, err error) error {
 		if err != nil {
 		if err != nil {
-			fr.log.Error("failed to load dashboard from ", "file", path, "error", err)
-			return nil
-		}
-
-		if dash.Dashboard.Id != 0 {
-			fr.log.Error("Cannot provision dashboard. Please remove the id property from the json file")
-			return nil
+			return err
 		}
 		}
 
 
-		cmd := &models.GetDashboardQuery{Slug: dash.Dashboard.Slug}
-		err = bus.Dispatch(cmd)
-
-		// if we don't have the dashboard in the db, save it!
-		if err == models.ErrDashboardNotFound {
-			fr.log.Debug("saving new dashboard", "file", path)
-			_, err = fr.dashboardRepo.SaveDashboard(dash)
+		isValid, err := validateWalkablePath(fileInfo)
+		if !isValid {
 			return err
 			return err
 		}
 		}
 
 
-		if err != nil {
-			fr.log.Error("failed to query for dashboard", "slug", dash.Dashboard.Slug, "error", err)
-			return nil
-		}
+		filesOnDisk[path] = fileInfo
+		return nil
+	}
+}
 
 
-		// break if db version is newer then fil version
-		if cmd.Result.Updated.Unix() >= fileInfo.ModTime().Unix() {
-			return nil
+func validateWalkablePath(fileInfo os.FileInfo) (bool, error) {
+	if fileInfo.IsDir() {
+		if strings.HasPrefix(fileInfo.Name(), ".") {
+			return false, filepath.SkipDir
 		}
 		}
+		return false, nil
+	}
 
 
-		fr.log.Debug("loading dashboard from disk into database.", "file", path)
-		_, err = fr.dashboardRepo.SaveDashboard(dash)
-
-		return err
+	if !strings.HasSuffix(fileInfo.Name(), ".json") {
+		return false, nil
 	}
 	}
+
+	return true, nil
 }
 }
 
 
-func (fr *fileReader) readDashboardFromFile(path string, folderId int64) (*dashboards.SaveDashboardItem, error) {
+func (fr *fileReader) readDashboardFromFile(path string, lastModified time.Time, folderId int64) (*dashboards.SaveDashboardDTO, error) {
 	reader, err := os.Open(path)
 	reader, err := os.Open(path)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
@@ -217,17 +290,53 @@ func (fr *fileReader) readDashboardFromFile(path string, folderId int64) (*dashb
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	stat, err := os.Stat(path)
+	dash, err := createDashboardJson(data, lastModified, fr.Cfg, folderId)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	dash, err := createDashboardJson(data, stat.ModTime(), fr.Cfg, folderId)
-	if err != nil {
-		return nil, err
+	return dash, nil
+}
+
+type provisioningMetadata struct {
+	uid   string
+	title string
+}
+
+func newProvisioningSanityChecker(provisioningProvider string) provisioningSanityChecker {
+	return provisioningSanityChecker{
+		provisioningProvider: provisioningProvider,
+		uidUsage:             map[string]uint8{},
+		titleUsage:           map[string]uint8{}}
+}
+
+type provisioningSanityChecker struct {
+	provisioningProvider string
+	uidUsage             map[string]uint8
+	titleUsage           map[string]uint8
+}
+
+func (checker provisioningSanityChecker) track(pm provisioningMetadata) {
+	if len(pm.uid) > 0 {
+		checker.uidUsage[pm.uid] += 1
+	}
+	if len(pm.title) > 0 {
+		checker.titleUsage[pm.title] += 1
 	}
 	}
 
 
-	fr.cache.addDashboardCache(path, dash)
+}
+
+func (checker provisioningSanityChecker) logWarnings(log log.Logger) {
+	for uid, times := range checker.uidUsage {
+		if times > 1 {
+			log.Error("the same 'uid' is used more than once", "uid", uid, "provider", checker.provisioningProvider)
+		}
+	}
+
+	for title, times := range checker.titleUsage {
+		if times > 1 {
+			log.Error("the same 'title' is used more than once", "title", title, "provider", checker.provisioningProvider)
+		}
+	}
 
 
-	return dash, nil
 }
 }

+ 43 - 48
pkg/services/provisioning/dashboards/file_reader_test.go

@@ -19,16 +19,16 @@ var (
 	brokenDashboards  string = "./test-dashboards/broken-dashboards"
 	brokenDashboards  string = "./test-dashboards/broken-dashboards"
 	oneDashboard      string = "./test-dashboards/one-dashboard"
 	oneDashboard      string = "./test-dashboards/one-dashboard"
 
 
-	fakeRepo *fakeDashboardRepo
+	fakeService *fakeDashboardProvisioningService
 )
 )
 
 
 func TestDashboardFileReader(t *testing.T) {
 func TestDashboardFileReader(t *testing.T) {
 	Convey("Dashboard file reader", t, func() {
 	Convey("Dashboard file reader", t, func() {
 		bus.ClearBusHandlers()
 		bus.ClearBusHandlers()
-		fakeRepo = &fakeDashboardRepo{}
+		origNewDashboardProvisioningService := dashboards.NewProvisioningService
+		fakeService = mockDashboardProvisioningService()
 
 
 		bus.AddHandler("test", mockGetDashboardQuery)
 		bus.AddHandler("test", mockGetDashboardQuery)
-		dashboards.SetRepository(fakeRepo)
 		logger := log.New("test.logger")
 		logger := log.New("test.logger")
 
 
 		Convey("Reading dashboards from disk", func() {
 		Convey("Reading dashboards from disk", func() {
@@ -54,7 +54,7 @@ func TestDashboardFileReader(t *testing.T) {
 				folders := 0
 				folders := 0
 				dashboards := 0
 				dashboards := 0
 
 
-				for _, i := range fakeRepo.inserted {
+				for _, i := range fakeService.inserted {
 					if i.Dashboard.IsFolder {
 					if i.Dashboard.IsFolder {
 						folders++
 						folders++
 					} else {
 					} else {
@@ -62,25 +62,8 @@ func TestDashboardFileReader(t *testing.T) {
 					}
 					}
 				}
 				}
 
 
-				So(dashboards, ShouldEqual, 2)
 				So(folders, ShouldEqual, 1)
 				So(folders, ShouldEqual, 1)
-			})
-
-			Convey("Should not update dashboards when db is newer", func() {
-				cfg.Options["path"] = oneDashboard
-
-				fakeRepo.getDashboard = append(fakeRepo.getDashboard, &models.Dashboard{
-					Updated: time.Now().Add(time.Hour),
-					Slug:    "grafana",
-				})
-
-				reader, err := NewDashboardFileReader(cfg, logger)
-				So(err, ShouldBeNil)
-
-				err = reader.startWalkingDisk()
-				So(err, ShouldBeNil)
-
-				So(len(fakeRepo.inserted), ShouldEqual, 0)
+				So(dashboards, ShouldEqual, 2)
 			})
 			})
 
 
 			Convey("Can read default dashboard and replace old version in database", func() {
 			Convey("Can read default dashboard and replace old version in database", func() {
@@ -88,7 +71,7 @@ func TestDashboardFileReader(t *testing.T) {
 
 
 				stat, _ := os.Stat(oneDashboard + "/dashboard1.json")
 				stat, _ := os.Stat(oneDashboard + "/dashboard1.json")
 
 
-				fakeRepo.getDashboard = append(fakeRepo.getDashboard, &models.Dashboard{
+				fakeService.getDashboard = append(fakeService.getDashboard, &models.Dashboard{
 					Updated: stat.ModTime().AddDate(0, 0, -1),
 					Updated: stat.ModTime().AddDate(0, 0, -1),
 					Slug:    "grafana",
 					Slug:    "grafana",
 				})
 				})
@@ -99,7 +82,7 @@ func TestDashboardFileReader(t *testing.T) {
 				err = reader.startWalkingDisk()
 				err = reader.startWalkingDisk()
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
 
 
-				So(len(fakeRepo.inserted), ShouldEqual, 1)
+				So(len(fakeService.inserted), ShouldEqual, 1)
 			})
 			})
 
 
 			Convey("Invalid configuration should return error", func() {
 			Convey("Invalid configuration should return error", func() {
@@ -133,7 +116,7 @@ func TestDashboardFileReader(t *testing.T) {
 				},
 				},
 			}
 			}
 
 
-			_, err := getOrCreateFolderId(cfg, fakeRepo)
+			_, err := getOrCreateFolderId(cfg, fakeService)
 			So(err, ShouldEqual, ErrFolderNameMissing)
 			So(err, ShouldEqual, ErrFolderNameMissing)
 		})
 		})
 
 
@@ -148,39 +131,28 @@ func TestDashboardFileReader(t *testing.T) {
 				},
 				},
 			}
 			}
 
 
-			folderId, err := getOrCreateFolderId(cfg, fakeRepo)
+			folderId, err := getOrCreateFolderId(cfg, fakeService)
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 			inserted := false
 			inserted := false
-			for _, d := range fakeRepo.inserted {
+			for _, d := range fakeService.inserted {
 				if d.Dashboard.IsFolder && d.Dashboard.Id == folderId {
 				if d.Dashboard.IsFolder && d.Dashboard.Id == folderId {
 					inserted = true
 					inserted = true
 				}
 				}
 			}
 			}
-			So(len(fakeRepo.inserted), ShouldEqual, 1)
+			So(len(fakeService.inserted), ShouldEqual, 1)
 			So(inserted, ShouldBeTrue)
 			So(inserted, ShouldBeTrue)
 		})
 		})
 
 
 		Convey("Walking the folder with dashboards", func() {
 		Convey("Walking the folder with dashboards", func() {
-			cfg := &DashboardsAsConfig{
-				Name:   "Default",
-				Type:   "file",
-				OrgId:  1,
-				Folder: "",
-				Options: map[string]interface{}{
-					"path": defaultDashboards,
-				},
-			}
-
-			reader, err := NewDashboardFileReader(cfg, log.New("test-logger"))
-			So(err, ShouldBeNil)
+			noFiles := map[string]os.FileInfo{}
 
 
 			Convey("should skip dirs that starts with .", func() {
 			Convey("should skip dirs that starts with .", func() {
-				shouldSkip := reader.createWalk(reader, 0)("path", &FakeFileInfo{isDirectory: true, name: ".folder"}, nil)
+				shouldSkip := createWalkFn(noFiles)("path", &FakeFileInfo{isDirectory: true, name: ".folder"}, nil)
 				So(shouldSkip, ShouldEqual, filepath.SkipDir)
 				So(shouldSkip, ShouldEqual, filepath.SkipDir)
 			})
 			})
 
 
 			Convey("should keep walking if file is not .json", func() {
 			Convey("should keep walking if file is not .json", func() {
-				shouldSkip := reader.createWalk(reader, 0)("path", &FakeFileInfo{isDirectory: true, name: "folder"}, nil)
+				shouldSkip := createWalkFn(noFiles)("path", &FakeFileInfo{isDirectory: true, name: "folder"}, nil)
 				So(shouldSkip, ShouldBeNil)
 				So(shouldSkip, ShouldBeNil)
 			})
 			})
 		})
 		})
@@ -208,6 +180,10 @@ func TestDashboardFileReader(t *testing.T) {
 				So(reader.Path, ShouldEqual, defaultDashboards)
 				So(reader.Path, ShouldEqual, defaultDashboards)
 			})
 			})
 		})
 		})
+
+		Reset(func() {
+			dashboards.NewProvisioningService = origNewDashboardProvisioningService
+		})
 	})
 	})
 }
 }
 
 
@@ -240,18 +216,37 @@ func (ffi FakeFileInfo) Sys() interface{} {
 	return nil
 	return nil
 }
 }
 
 
-type fakeDashboardRepo struct {
-	inserted     []*dashboards.SaveDashboardItem
+func mockDashboardProvisioningService() *fakeDashboardProvisioningService {
+	mock := fakeDashboardProvisioningService{}
+	dashboards.NewProvisioningService = func() dashboards.DashboardProvisioningService {
+		return &mock
+	}
+	return &mock
+}
+
+type fakeDashboardProvisioningService struct {
+	inserted     []*dashboards.SaveDashboardDTO
+	provisioned  []*models.DashboardProvisioning
 	getDashboard []*models.Dashboard
 	getDashboard []*models.Dashboard
 }
 }
 
 
-func (repo *fakeDashboardRepo) SaveDashboard(json *dashboards.SaveDashboardItem) (*models.Dashboard, error) {
-	repo.inserted = append(repo.inserted, json)
-	return json.Dashboard, nil
+func (s *fakeDashboardProvisioningService) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) {
+	return s.provisioned, nil
+}
+
+func (s *fakeDashboardProvisioningService) SaveProvisionedDashboard(dto *dashboards.SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) {
+	s.inserted = append(s.inserted, dto)
+	s.provisioned = append(s.provisioned, provisioning)
+	return dto.Dashboard, nil
+}
+
+func (s *fakeDashboardProvisioningService) SaveFolderForProvisionedDashboards(dto *dashboards.SaveDashboardDTO) (*models.Dashboard, error) {
+	s.inserted = append(s.inserted, dto)
+	return dto.Dashboard, nil
 }
 }
 
 
 func mockGetDashboardQuery(cmd *models.GetDashboardQuery) error {
 func mockGetDashboardQuery(cmd *models.GetDashboardQuery) error {
-	for _, d := range fakeRepo.getDashboard {
+	for _, d := range fakeService.getDashboard {
 		if d.Slug == cmd.Slug {
 		if d.Slug == cmd.Slug {
 			cmd.Result = d
 			cmd.Result = d
 			return nil
 			return nil

+ 5 - 1
pkg/services/provisioning/dashboards/test-configs/dashboards-from-disk/dev-dashboards.yaml

@@ -1,7 +1,11 @@
+apiVersion: 1
+
+providers:
 - name: 'general dashboards'
 - name: 'general dashboards'
-  org_id: 2
+  orgId: 2
   folder: 'developers'
   folder: 'developers'
   editable: true
   editable: true
+  disableDeletion: true
   type: file
   type: file
   options:
   options:
     path: /var/lib/grafana/dashboards
     path: /var/lib/grafana/dashboards

+ 10 - 0
pkg/services/provisioning/dashboards/test-configs/dashboards-from-disk/sample.yaml

@@ -0,0 +1,10 @@
+apiVersion: 1
+
+#providers:
+#- name: 'gasdf'
+#  orgId: 2
+#  folder: 'developers'
+#  editable: true
+#  type: file
+#  options:
+#    path: /var/lib/grafana/dashboards

+ 13 - 0
pkg/services/provisioning/dashboards/test-configs/version-0/version-0.yaml

@@ -0,0 +1,13 @@
+- name: 'general dashboards'
+  org_id: 2
+  folder: 'developers'
+  editable: true
+  disableDeletion: true
+  type: file
+  options:
+    path: /var/lib/grafana/dashboards
+
+- name: 'default'
+  type: file
+  options:
+    path: /var/lib/grafana/dashboards

+ 1 - 2
pkg/services/provisioning/dashboards/test-dashboards/folder-one/dashboard1.json

@@ -1,5 +1,5 @@
 {
 {
-    "title": "Grafana",
+    "title": "Grafana1",
     "tags": [],
     "tags": [],
     "style": "dark",
     "style": "dark",
     "timezone": "browser",
     "timezone": "browser",
@@ -170,4 +170,3 @@
     },
     },
     "version": 5
     "version": 5
   }
   }
-  

+ 1 - 2
pkg/services/provisioning/dashboards/test-dashboards/folder-one/dashboard2.json

@@ -1,5 +1,5 @@
 {
 {
-    "title": "Grafana",
+    "title": "Grafana2",
     "tags": [],
     "tags": [],
     "style": "dark",
     "style": "dark",
     "timezone": "browser",
     "timezone": "browser",
@@ -170,4 +170,3 @@
     },
     },
     "version": 5
     "version": 5
   }
   }
-  

+ 73 - 8
pkg/services/provisioning/dashboards/types.go

@@ -10,16 +10,45 @@ import (
 )
 )
 
 
 type DashboardsAsConfig struct {
 type DashboardsAsConfig struct {
-	Name     string                 `json:"name" yaml:"name"`
-	Type     string                 `json:"type" yaml:"type"`
-	OrgId    int64                  `json:"org_id" yaml:"org_id"`
-	Folder   string                 `json:"folder" yaml:"folder"`
-	Editable bool                   `json:"editable" yaml:"editable"`
-	Options  map[string]interface{} `json:"options" yaml:"options"`
+	Name            string
+	Type            string
+	OrgId           int64
+	Folder          string
+	Editable        bool
+	Options         map[string]interface{}
+	DisableDeletion bool
 }
 }
 
 
-func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *DashboardsAsConfig, folderId int64) (*dashboards.SaveDashboardItem, error) {
-	dash := &dashboards.SaveDashboardItem{}
+type DashboardsAsConfigV0 struct {
+	Name            string                 `json:"name" yaml:"name"`
+	Type            string                 `json:"type" yaml:"type"`
+	OrgId           int64                  `json:"org_id" yaml:"org_id"`
+	Folder          string                 `json:"folder" yaml:"folder"`
+	Editable        bool                   `json:"editable" yaml:"editable"`
+	Options         map[string]interface{} `json:"options" yaml:"options"`
+	DisableDeletion bool                   `json:"disableDeletion" yaml:"disableDeletion"`
+}
+
+type ConfigVersion struct {
+	ApiVersion int64 `json:"apiVersion" yaml:"apiVersion"`
+}
+
+type DashboardAsConfigV1 struct {
+	Providers []*DashboardProviderConfigs `json:"providers" yaml:"providers"`
+}
+
+type DashboardProviderConfigs struct {
+	Name            string                 `json:"name" yaml:"name"`
+	Type            string                 `json:"type" yaml:"type"`
+	OrgId           int64                  `json:"orgId" yaml:"orgId"`
+	Folder          string                 `json:"folder" yaml:"folder"`
+	Editable        bool                   `json:"editable" yaml:"editable"`
+	Options         map[string]interface{} `json:"options" yaml:"options"`
+	DisableDeletion bool                   `json:"disableDeletion" yaml:"disableDeletion"`
+}
+
+func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *DashboardsAsConfig, folderId int64) (*dashboards.SaveDashboardDTO, error) {
+	dash := &dashboards.SaveDashboardDTO{}
 	dash.Dashboard = models.NewDashboardFromJson(data)
 	dash.Dashboard = models.NewDashboardFromJson(data)
 	dash.UpdatedAt = lastModified
 	dash.UpdatedAt = lastModified
 	dash.Overwrite = true
 	dash.Overwrite = true
@@ -36,3 +65,39 @@ func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *Das
 
 
 	return dash, nil
 	return dash, nil
 }
 }
+
+func mapV0ToDashboardAsConfig(v0 []*DashboardsAsConfigV0) []*DashboardsAsConfig {
+	var r []*DashboardsAsConfig
+
+	for _, v := range v0 {
+		r = append(r, &DashboardsAsConfig{
+			Name:            v.Name,
+			Type:            v.Type,
+			OrgId:           v.OrgId,
+			Folder:          v.Folder,
+			Editable:        v.Editable,
+			Options:         v.Options,
+			DisableDeletion: v.DisableDeletion,
+		})
+	}
+
+	return r
+}
+
+func (dc *DashboardAsConfigV1) mapToDashboardAsConfig() []*DashboardsAsConfig {
+	var r []*DashboardsAsConfig
+
+	for _, v := range dc.Providers {
+		r = append(r, &DashboardsAsConfig{
+			Name:            v.Name,
+			Type:            v.Type,
+			OrgId:           v.OrgId,
+			Folder:          v.Folder,
+			Editable:        v.Editable,
+			Options:         v.Options,
+			DisableDeletion: v.DisableDeletion,
+		})
+	}
+
+	return r
+}

+ 113 - 0
pkg/services/provisioning/datasources/config_reader.go

@@ -0,0 +1,113 @@
+package datasources
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/grafana/grafana/pkg/log"
+	"gopkg.in/yaml.v2"
+)
+
+type configReader struct {
+	log log.Logger
+}
+
+func (cr *configReader) readConfig(path string) ([]*DatasourcesAsConfig, error) {
+	var datasources []*DatasourcesAsConfig
+
+	files, err := ioutil.ReadDir(path)
+	if err != nil {
+		cr.log.Error("cant read datasource provisioning files from directory", "path", path)
+		return datasources, nil
+	}
+
+	for _, file := range files {
+		if strings.HasSuffix(file.Name(), ".yaml") || strings.HasSuffix(file.Name(), ".yml") {
+			datasource, err := cr.parseDatasourceConfig(path, file)
+			if err != nil {
+				return nil, err
+			}
+
+			if datasource != nil {
+				datasources = append(datasources, datasource)
+			}
+		}
+	}
+
+	err = validateDefaultUniqueness(datasources)
+	if err != nil {
+		return nil, err
+	}
+
+	return datasources, nil
+}
+
+func (cr *configReader) parseDatasourceConfig(path string, file os.FileInfo) (*DatasourcesAsConfig, error) {
+	filename, _ := filepath.Abs(filepath.Join(path, file.Name()))
+	yamlFile, err := ioutil.ReadFile(filename)
+	if err != nil {
+		return nil, err
+	}
+
+	var apiVersion *ConfigVersion
+	err = yaml.Unmarshal(yamlFile, &apiVersion)
+	if err != nil {
+		return nil, err
+	}
+
+	if apiVersion == nil {
+		apiVersion = &ConfigVersion{ApiVersion: 0}
+	}
+
+	if apiVersion.ApiVersion > 0 {
+		var v1 *DatasourcesAsConfigV1
+		err = yaml.Unmarshal(yamlFile, &v1)
+		if err != nil {
+			return nil, err
+		}
+
+		return v1.mapToDatasourceFromConfig(apiVersion.ApiVersion), nil
+	}
+
+	var v0 *DatasourcesAsConfigV0
+	err = yaml.Unmarshal(yamlFile, &v0)
+	if err != nil {
+		return nil, err
+	}
+
+	cr.log.Warn("[Deprecated] the datasource provisioning config is outdated. please upgrade", "filename", filename)
+
+	return v0.mapToDatasourceFromConfig(apiVersion.ApiVersion), nil
+}
+
+func validateDefaultUniqueness(datasources []*DatasourcesAsConfig) error {
+	defaultCount := 0
+	for i := range datasources {
+		if datasources[i].Datasources == nil {
+			continue
+		}
+
+		for _, ds := range datasources[i].Datasources {
+			if ds.OrgId == 0 {
+				ds.OrgId = 1
+			}
+
+			if ds.IsDefault {
+				defaultCount++
+				if defaultCount > 1 {
+					return ErrInvalidConfigToManyDefault
+				}
+			}
+		}
+
+		for _, ds := range datasources[i].DeleteDatasources {
+			if ds.OrgId == 0 {
+				ds.OrgId = 1
+			}
+		}
+	}
+
+	return nil
+}

+ 70 - 31
pkg/services/provisioning/datasources/datasources_test.go → pkg/services/provisioning/datasources/config_reader_test.go

@@ -17,6 +17,7 @@ var (
 	twoDatasourcesConfigPurgeOthers string     = "./test-configs/insert-two-delete-two"
 	twoDatasourcesConfigPurgeOthers string     = "./test-configs/insert-two-delete-two"
 	doubleDatasourcesConfig         string     = "./test-configs/double-default"
 	doubleDatasourcesConfig         string     = "./test-configs/double-default"
 	allProperties                   string     = "./test-configs/all-properties"
 	allProperties                   string     = "./test-configs/all-properties"
+	versionZero                     string     = "./test-configs/version-0"
 	brokenYaml                      string     = "./test-configs/broken-yaml"
 	brokenYaml                      string     = "./test-configs/broken-yaml"
 
 
 	fakeRepo *fakeRepository
 	fakeRepo *fakeRepository
@@ -130,48 +131,86 @@ func TestDatasourceAsConfig(t *testing.T) {
 			So(len(cfg), ShouldEqual, 0)
 			So(len(cfg), ShouldEqual, 0)
 		})
 		})
 
 
-		Convey("can read all properties", func() {
+		Convey("can read all properties from version 1", func() {
 			cfgProvifer := &configReader{log: log.New("test logger")}
 			cfgProvifer := &configReader{log: log.New("test logger")}
 			cfg, err := cfgProvifer.readConfig(allProperties)
 			cfg, err := cfgProvifer.readConfig(allProperties)
 			if err != nil {
 			if err != nil {
 				t.Fatalf("readConfig return an error %v", err)
 				t.Fatalf("readConfig return an error %v", err)
 			}
 			}
 
 
-			So(len(cfg), ShouldEqual, 2)
+			So(len(cfg), ShouldEqual, 3)
 
 
 			dsCfg := cfg[0]
 			dsCfg := cfg[0]
-			ds := dsCfg.Datasources[0]
-
-			So(ds.Name, ShouldEqual, "name")
-			So(ds.Type, ShouldEqual, "type")
-			So(ds.Access, ShouldEqual, models.DS_ACCESS_PROXY)
-			So(ds.OrgId, ShouldEqual, 2)
-			So(ds.Url, ShouldEqual, "url")
-			So(ds.User, ShouldEqual, "user")
-			So(ds.Password, ShouldEqual, "password")
-			So(ds.Database, ShouldEqual, "database")
-			So(ds.BasicAuth, ShouldBeTrue)
-			So(ds.BasicAuthUser, ShouldEqual, "basic_auth_user")
-			So(ds.BasicAuthPassword, ShouldEqual, "basic_auth_password")
-			So(ds.WithCredentials, ShouldBeTrue)
-			So(ds.IsDefault, ShouldBeTrue)
-			So(ds.Editable, ShouldBeTrue)
-
-			So(len(ds.JsonData), ShouldBeGreaterThan, 2)
-			So(ds.JsonData["graphiteVersion"], ShouldEqual, "1.1")
-			So(ds.JsonData["tlsAuth"], ShouldEqual, true)
-			So(ds.JsonData["tlsAuthWithCACert"], ShouldEqual, true)
-
-			So(len(ds.SecureJsonData), ShouldBeGreaterThan, 2)
-			So(ds.SecureJsonData["tlsCACert"], ShouldEqual, "MjNOcW9RdkbUDHZmpco2HCYzVq9dE+i6Yi+gmUJotq5CDA==")
-			So(ds.SecureJsonData["tlsClientCert"], ShouldEqual, "ckN0dGlyMXN503YNfjTcf9CV+GGQneN+xmAclQ==")
-			So(ds.SecureJsonData["tlsClientKey"], ShouldEqual, "ZkN4aG1aNkja/gKAB1wlnKFIsy2SRDq4slrM0A==")
-
-			dstwo := cfg[1].Datasources[0]
-			So(dstwo.Name, ShouldEqual, "name2")
+
+			So(dsCfg.ApiVersion, ShouldEqual, 1)
+
+			validateDatasource(dsCfg)
+			validateDeleteDatasources(dsCfg)
+
+			dsCount := 0
+			delDsCount := 0
+
+			for _, c := range cfg {
+				dsCount += len(c.Datasources)
+				delDsCount += len(c.DeleteDatasources)
+			}
+
+			So(dsCount, ShouldEqual, 2)
+			So(delDsCount, ShouldEqual, 1)
+		})
+
+		Convey("can read all properties from version 0", func() {
+			cfgProvifer := &configReader{log: log.New("test logger")}
+			cfg, err := cfgProvifer.readConfig(versionZero)
+			if err != nil {
+				t.Fatalf("readConfig return an error %v", err)
+			}
+
+			So(len(cfg), ShouldEqual, 1)
+
+			dsCfg := cfg[0]
+
+			So(dsCfg.ApiVersion, ShouldEqual, 0)
+
+			validateDatasource(dsCfg)
+			validateDeleteDatasources(dsCfg)
 		})
 		})
 	})
 	})
 }
 }
+func validateDeleteDatasources(dsCfg *DatasourcesAsConfig) {
+	So(len(dsCfg.DeleteDatasources), ShouldEqual, 1)
+	deleteDs := dsCfg.DeleteDatasources[0]
+	So(deleteDs.Name, ShouldEqual, "old-graphite3")
+	So(deleteDs.OrgId, ShouldEqual, 2)
+}
+func validateDatasource(dsCfg *DatasourcesAsConfig) {
+	ds := dsCfg.Datasources[0]
+	So(ds.Name, ShouldEqual, "name")
+	So(ds.Type, ShouldEqual, "type")
+	So(ds.Access, ShouldEqual, models.DS_ACCESS_PROXY)
+	So(ds.OrgId, ShouldEqual, 2)
+	So(ds.Url, ShouldEqual, "url")
+	So(ds.User, ShouldEqual, "user")
+	So(ds.Password, ShouldEqual, "password")
+	So(ds.Database, ShouldEqual, "database")
+	So(ds.BasicAuth, ShouldBeTrue)
+	So(ds.BasicAuthUser, ShouldEqual, "basic_auth_user")
+	So(ds.BasicAuthPassword, ShouldEqual, "basic_auth_password")
+	So(ds.WithCredentials, ShouldBeTrue)
+	So(ds.IsDefault, ShouldBeTrue)
+	So(ds.Editable, ShouldBeTrue)
+	So(ds.Version, ShouldEqual, 10)
+
+	So(len(ds.JsonData), ShouldBeGreaterThan, 2)
+	So(ds.JsonData["graphiteVersion"], ShouldEqual, "1.1")
+	So(ds.JsonData["tlsAuth"], ShouldEqual, true)
+	So(ds.JsonData["tlsAuthWithCACert"], ShouldEqual, true)
+
+	So(len(ds.SecureJsonData), ShouldBeGreaterThan, 2)
+	So(ds.SecureJsonData["tlsCACert"], ShouldEqual, "MjNOcW9RdkbUDHZmpco2HCYzVq9dE+i6Yi+gmUJotq5CDA==")
+	So(ds.SecureJsonData["tlsClientCert"], ShouldEqual, "ckN0dGlyMXN503YNfjTcf9CV+GGQneN+xmAclQ==")
+	So(ds.SecureJsonData["tlsClientKey"], ShouldEqual, "ZkN4aG1aNkja/gKAB1wlnKFIsy2SRDq4slrM0A==")
+}
 
 
 type fakeRepository struct {
 type fakeRepository struct {
 	inserted []*models.AddDataSourceCommand
 	inserted []*models.AddDataSourceCommand

+ 0 - 66
pkg/services/provisioning/datasources/datasources.go

@@ -2,16 +2,12 @@ package datasources
 
 
 import (
 import (
 	"errors"
 	"errors"
-	"io/ioutil"
-	"path/filepath"
-	"strings"
 
 
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
 
 
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
 
 
 	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/models"
-	yaml "gopkg.in/yaml.v2"
 )
 )
 
 
 var (
 var (
@@ -94,65 +90,3 @@ func (dc *DatasourceProvisioner) deleteDatasources(dsToDelete []*DeleteDatasourc
 
 
 	return nil
 	return nil
 }
 }
-
-type configReader struct {
-	log log.Logger
-}
-
-func (cr *configReader) readConfig(path string) ([]*DatasourcesAsConfig, error) {
-	var datasources []*DatasourcesAsConfig
-
-	files, err := ioutil.ReadDir(path)
-	if err != nil {
-		cr.log.Error("cant read datasource provisioning files from directory", "path", path)
-		return datasources, nil
-	}
-
-	for _, file := range files {
-		if strings.HasSuffix(file.Name(), ".yaml") || strings.HasSuffix(file.Name(), ".yml") {
-			filename, _ := filepath.Abs(filepath.Join(path, file.Name()))
-			yamlFile, err := ioutil.ReadFile(filename)
-
-			if err != nil {
-				return nil, err
-			}
-			var datasource *DatasourcesAsConfig
-			err = yaml.Unmarshal(yamlFile, &datasource)
-			if err != nil {
-				return nil, err
-			}
-
-			if datasource != nil {
-				datasources = append(datasources, datasource)
-			}
-		}
-	}
-
-	defaultCount := 0
-	for i := range datasources {
-		if datasources[i].Datasources == nil {
-			continue
-		}
-
-		for _, ds := range datasources[i].Datasources {
-			if ds.OrgId == 0 {
-				ds.OrgId = 1
-			}
-
-			if ds.IsDefault {
-				defaultCount++
-				if defaultCount > 1 {
-					return nil, ErrInvalidConfigToManyDefault
-				}
-			}
-		}
-
-		for _, ds := range datasources[i].DeleteDatasources {
-			if ds.OrgId == 0 {
-				ds.OrgId = 1
-			}
-		}
-	}
-
-	return datasources, nil
-}

+ 15 - 8
pkg/services/provisioning/datasources/test-configs/all-properties/all-properties.yaml

@@ -1,23 +1,30 @@
+apiVersion: 1
+
 datasources:
 datasources:
   - name: name
   - name: name
     type: type
     type: type
     access: proxy
     access: proxy
-    org_id: 2
+    orgId: 2
     url: url
     url: url
     password: password
     password: password
     user: user
     user: user
     database: database
     database: database
-    basic_auth: true
-    basic_auth_user: basic_auth_user
-    basic_auth_password: basic_auth_password
-    with_credentials: true
-    is_default: true
-    json_data: 
+    basicAuth: true
+    basicAuthUser: basic_auth_user
+    basicAuthPassword: basic_auth_password
+    withCredentials: true
+    isDefault: true
+    jsonData:
       graphiteVersion: "1.1"
       graphiteVersion: "1.1"
       tlsAuth: true
       tlsAuth: true
       tlsAuthWithCACert: true
       tlsAuthWithCACert: true
-    secure_json_data:
+    secureJsonData:
       tlsCACert: "MjNOcW9RdkbUDHZmpco2HCYzVq9dE+i6Yi+gmUJotq5CDA=="
       tlsCACert: "MjNOcW9RdkbUDHZmpco2HCYzVq9dE+i6Yi+gmUJotq5CDA=="
       tlsClientCert: "ckN0dGlyMXN503YNfjTcf9CV+GGQneN+xmAclQ=="
       tlsClientCert: "ckN0dGlyMXN503YNfjTcf9CV+GGQneN+xmAclQ=="
       tlsClientKey: "ZkN4aG1aNkja/gKAB1wlnKFIsy2SRDq4slrM0A=="
       tlsClientKey: "ZkN4aG1aNkja/gKAB1wlnKFIsy2SRDq4slrM0A=="
     editable: true
     editable: true
+    version: 10
+
+deleteDatasources:
+  - name: old-graphite3
+    orgId: 2

+ 32 - 0
pkg/services/provisioning/datasources/test-configs/all-properties/sample.yaml

@@ -0,0 +1,32 @@
+# Should not be included
+
+
+apiVersion: 1
+
+#datasources:
+#  - name: name
+#    type: type
+#    access: proxy
+#    orgId: 2
+#    url: url
+#    password: password
+#    user: user
+#    database: database
+#    basicAuth: true
+#    basicAuthUser: basic_auth_user
+#    basicAuthPassword: basic_auth_password
+#    withCredentials: true
+#    jsonData:
+#      graphiteVersion: "1.1"
+#      tlsAuth: true
+#      tlsAuthWithCACert: true
+#    secureJsonData:
+#      tlsCACert: "MjNOcW9RdkbUDHZmpco2HCYzVq9dE+i6Yi+gmUJotq5CDA=="
+#      tlsClientCert: "ckN0dGlyMXN503YNfjTcf9CV+GGQneN+xmAclQ=="
+#      tlsClientKey: "ZkN4aG1aNkja/gKAB1wlnKFIsy2SRDq4slrM0A=="
+#    editable: true
+#    version: 10
+#
+#deleteDatasources:
+#  - name: old-graphite3
+#    orgId: 2

+ 1 - 1
pkg/services/provisioning/datasources/test-configs/all-properties/second.yaml

@@ -3,5 +3,5 @@ datasources:
   - name: name2
   - name: name2
     type: type2
     type: type2
     access: proxy
     access: proxy
-    org_id: 2
+    orgId: 2
     url: url2
     url: url2

+ 28 - 0
pkg/services/provisioning/datasources/test-configs/version-0/version-0.yaml

@@ -0,0 +1,28 @@
+datasources:
+  - name: name
+    type: type
+    access: proxy
+    org_id: 2
+    url: url
+    password: password
+    user: user
+    database: database
+    basic_auth: true
+    basic_auth_user: basic_auth_user
+    basic_auth_password: basic_auth_password
+    with_credentials: true
+    is_default: true
+    json_data:
+      graphiteVersion: "1.1"
+      tlsAuth: true
+      tlsAuthWithCACert: true
+    secure_json_data:
+      tlsCACert: "MjNOcW9RdkbUDHZmpco2HCYzVq9dE+i6Yi+gmUJotq5CDA=="
+      tlsClientCert: "ckN0dGlyMXN503YNfjTcf9CV+GGQneN+xmAclQ=="
+      tlsClientKey: "ZkN4aG1aNkja/gKAB1wlnKFIsy2SRDq4slrM0A=="
+    editable: true
+    version: 10
+
+delete_datasources:
+  - name: old-graphite3
+    org_id: 2

+ 160 - 6
pkg/services/provisioning/datasources/types.go

@@ -1,22 +1,74 @@
 package datasources
 package datasources
 
 
-import "github.com/grafana/grafana/pkg/models"
+import (
+	"github.com/grafana/grafana/pkg/models"
+)
 import "github.com/grafana/grafana/pkg/components/simplejson"
 import "github.com/grafana/grafana/pkg/components/simplejson"
 
 
+type ConfigVersion struct {
+	ApiVersion int64 `json:"apiVersion" yaml:"apiVersion"`
+}
+
 type DatasourcesAsConfig struct {
 type DatasourcesAsConfig struct {
-	Datasources       []*DataSourceFromConfig   `json:"datasources" yaml:"datasources"`
-	DeleteDatasources []*DeleteDatasourceConfig `json:"delete_datasources" yaml:"delete_datasources"`
+	ApiVersion int64
+
+	Datasources       []*DataSourceFromConfig
+	DeleteDatasources []*DeleteDatasourceConfig
 }
 }
 
 
 type DeleteDatasourceConfig struct {
 type DeleteDatasourceConfig struct {
+	OrgId int64
+	Name  string
+}
+
+type DataSourceFromConfig struct {
+	OrgId   int64
+	Version int
+
+	Name              string
+	Type              string
+	Access            string
+	Url               string
+	Password          string
+	User              string
+	Database          string
+	BasicAuth         bool
+	BasicAuthUser     string
+	BasicAuthPassword string
+	WithCredentials   bool
+	IsDefault         bool
+	JsonData          map[string]interface{}
+	SecureJsonData    map[string]string
+	Editable          bool
+}
+
+type DatasourcesAsConfigV0 struct {
+	ConfigVersion
+
+	Datasources       []*DataSourceFromConfigV0   `json:"datasources" yaml:"datasources"`
+	DeleteDatasources []*DeleteDatasourceConfigV0 `json:"delete_datasources" yaml:"delete_datasources"`
+}
+
+type DatasourcesAsConfigV1 struct {
+	ConfigVersion
+
+	Datasources       []*DataSourceFromConfigV1   `json:"datasources" yaml:"datasources"`
+	DeleteDatasources []*DeleteDatasourceConfigV1 `json:"deleteDatasources" yaml:"deleteDatasources"`
+}
+
+type DeleteDatasourceConfigV0 struct {
 	OrgId int64  `json:"org_id" yaml:"org_id"`
 	OrgId int64  `json:"org_id" yaml:"org_id"`
 	Name  string `json:"name" yaml:"name"`
 	Name  string `json:"name" yaml:"name"`
 }
 }
 
 
-type DataSourceFromConfig struct {
-	OrgId   int64 `json:"org_id" yaml:"org_id"`
-	Version int   `json:"version" yaml:"version"`
+type DeleteDatasourceConfigV1 struct {
+	OrgId int64  `json:"orgId" yaml:"orgId"`
+	Name  string `json:"name" yaml:"name"`
+}
 
 
+type DataSourceFromConfigV0 struct {
+	OrgId             int64                  `json:"org_id" yaml:"org_id"`
+	Version           int                    `json:"version" yaml:"version"`
 	Name              string                 `json:"name" yaml:"name"`
 	Name              string                 `json:"name" yaml:"name"`
 	Type              string                 `json:"type" yaml:"type"`
 	Type              string                 `json:"type" yaml:"type"`
 	Access            string                 `json:"access" yaml:"access"`
 	Access            string                 `json:"access" yaml:"access"`
@@ -34,6 +86,108 @@ type DataSourceFromConfig struct {
 	Editable          bool                   `json:"editable" yaml:"editable"`
 	Editable          bool                   `json:"editable" yaml:"editable"`
 }
 }
 
 
+type DataSourceFromConfigV1 struct {
+	OrgId             int64                  `json:"orgId" yaml:"orgId"`
+	Version           int                    `json:"version" yaml:"version"`
+	Name              string                 `json:"name" yaml:"name"`
+	Type              string                 `json:"type" yaml:"type"`
+	Access            string                 `json:"access" yaml:"access"`
+	Url               string                 `json:"url" yaml:"url"`
+	Password          string                 `json:"password" yaml:"password"`
+	User              string                 `json:"user" yaml:"user"`
+	Database          string                 `json:"database" yaml:"database"`
+	BasicAuth         bool                   `json:"basicAuth" yaml:"basicAuth"`
+	BasicAuthUser     string                 `json:"basicAuthUser" yaml:"basicAuthUser"`
+	BasicAuthPassword string                 `json:"basicAuthPassword" yaml:"basicAuthPassword"`
+	WithCredentials   bool                   `json:"withCredentials" yaml:"withCredentials"`
+	IsDefault         bool                   `json:"isDefault" yaml:"isDefault"`
+	JsonData          map[string]interface{} `json:"jsonData" yaml:"jsonData"`
+	SecureJsonData    map[string]string      `json:"secureJsonData" yaml:"secureJsonData"`
+	Editable          bool                   `json:"editable" yaml:"editable"`
+}
+
+func (cfg *DatasourcesAsConfigV1) mapToDatasourceFromConfig(apiVersion int64) *DatasourcesAsConfig {
+	r := &DatasourcesAsConfig{}
+
+	r.ApiVersion = apiVersion
+
+	if cfg == nil {
+		return r
+	}
+
+	for _, ds := range cfg.Datasources {
+		r.Datasources = append(r.Datasources, &DataSourceFromConfig{
+			OrgId:             ds.OrgId,
+			Name:              ds.Name,
+			Type:              ds.Type,
+			Access:            ds.Access,
+			Url:               ds.Url,
+			Password:          ds.Password,
+			User:              ds.User,
+			Database:          ds.Database,
+			BasicAuth:         ds.BasicAuth,
+			BasicAuthUser:     ds.BasicAuthUser,
+			BasicAuthPassword: ds.BasicAuthPassword,
+			WithCredentials:   ds.WithCredentials,
+			IsDefault:         ds.IsDefault,
+			JsonData:          ds.JsonData,
+			SecureJsonData:    ds.SecureJsonData,
+			Editable:          ds.Editable,
+			Version:           ds.Version,
+		})
+	}
+
+	for _, ds := range cfg.DeleteDatasources {
+		r.DeleteDatasources = append(r.DeleteDatasources, &DeleteDatasourceConfig{
+			OrgId: ds.OrgId,
+			Name:  ds.Name,
+		})
+	}
+
+	return r
+}
+
+func (cfg *DatasourcesAsConfigV0) mapToDatasourceFromConfig(apiVersion int64) *DatasourcesAsConfig {
+	r := &DatasourcesAsConfig{}
+
+	r.ApiVersion = apiVersion
+
+	if cfg == nil {
+		return r
+	}
+
+	for _, ds := range cfg.Datasources {
+		r.Datasources = append(r.Datasources, &DataSourceFromConfig{
+			OrgId:             ds.OrgId,
+			Name:              ds.Name,
+			Type:              ds.Type,
+			Access:            ds.Access,
+			Url:               ds.Url,
+			Password:          ds.Password,
+			User:              ds.User,
+			Database:          ds.Database,
+			BasicAuth:         ds.BasicAuth,
+			BasicAuthUser:     ds.BasicAuthUser,
+			BasicAuthPassword: ds.BasicAuthPassword,
+			WithCredentials:   ds.WithCredentials,
+			IsDefault:         ds.IsDefault,
+			JsonData:          ds.JsonData,
+			SecureJsonData:    ds.SecureJsonData,
+			Editable:          ds.Editable,
+			Version:           ds.Version,
+		})
+	}
+
+	for _, ds := range cfg.DeleteDatasources {
+		r.DeleteDatasources = append(r.DeleteDatasources, &DeleteDatasourceConfig{
+			OrgId: ds.OrgId,
+			Name:  ds.Name,
+		})
+	}
+
+	return r
+}
+
 func createInsertCommand(ds *DataSourceFromConfig) *models.AddDataSourceCommand {
 func createInsertCommand(ds *DataSourceFromConfig) *models.AddDataSourceCommand {
 	jsonData := simplejson.New()
 	jsonData := simplejson.New()
 	if len(ds.JsonData) > 0 {
 	if len(ds.JsonData) > 0 {

+ 1 - 0
pkg/services/search/handlers.go

@@ -21,6 +21,7 @@ func searchHandler(query *Query) error {
 		FolderIds:    query.FolderIds,
 		FolderIds:    query.FolderIds,
 		Tags:         query.Tags,
 		Tags:         query.Tags,
 		Limit:        query.Limit,
 		Limit:        query.Limit,
+		Permission:   query.Permission,
 	}
 	}
 
 
 	if err := bus.Dispatch(&dashQuery); err != nil {
 	if err := bus.Dispatch(&dashQuery); err != nil {

+ 2 - 1
pkg/services/search/models.go

@@ -52,6 +52,7 @@ type Query struct {
 	Type         string
 	Type         string
 	DashboardIds []int64
 	DashboardIds []int64
 	FolderIds    []int64
 	FolderIds    []int64
+	Permission   models.PermissionType
 
 
 	Result HitList
 	Result HitList
 }
 }
@@ -66,7 +67,7 @@ type FindPersistedDashboardsQuery struct {
 	FolderIds    []int64
 	FolderIds    []int64
 	Tags         []string
 	Tags         []string
 	Limit        int
 	Limit        int
-	IsBrowse     bool
+	Permission   models.PermissionType
 
 
 	Result HitList
 	Result HitList
 }
 }

+ 33 - 24
pkg/services/sqlstore/alert.go

@@ -61,52 +61,61 @@ func deleteAlertByIdInternal(alertId int64, reason string, sess *DBSession) erro
 }
 }
 
 
 func HandleAlertsQuery(query *m.GetAlertsQuery) error {
 func HandleAlertsQuery(query *m.GetAlertsQuery) error {
-	var sql bytes.Buffer
-	params := make([]interface{}, 0)
-
-	sql.WriteString(`SELECT *
-						from alert
-						`)
-
-	sql.WriteString(`WHERE org_id = ?`)
-	params = append(params, query.OrgId)
+	builder := SqlBuilder{}
+
+	builder.Write(`SELECT
+		alert.id,
+		alert.dashboard_id,
+		alert.panel_id,
+		alert.name,
+		alert.state,
+		alert.new_state_date,
+		alert.eval_date,
+		alert.execution_error,
+		dashboard.uid as dashboard_uid,
+		dashboard.slug as dashboard_slug
+		FROM alert
+		INNER JOIN dashboard on dashboard.id = alert.dashboard_id `)
+
+	builder.Write(`WHERE alert.org_id = ?`, query.OrgId)
 
 
 	if query.DashboardId != 0 {
 	if query.DashboardId != 0 {
-		sql.WriteString(` AND dashboard_id = ?`)
-		params = append(params, query.DashboardId)
+		builder.Write(` AND alert.dashboard_id = ?`, query.DashboardId)
 	}
 	}
 
 
 	if query.PanelId != 0 {
 	if query.PanelId != 0 {
-		sql.WriteString(` AND panel_id = ?`)
-		params = append(params, query.PanelId)
+		builder.Write(` AND alert.panel_id = ?`, query.PanelId)
 	}
 	}
 
 
 	if len(query.State) > 0 && query.State[0] != "all" {
 	if len(query.State) > 0 && query.State[0] != "all" {
-		sql.WriteString(` AND (`)
+		builder.Write(` AND (`)
 		for i, v := range query.State {
 		for i, v := range query.State {
 			if i > 0 {
 			if i > 0 {
-				sql.WriteString(" OR ")
+				builder.Write(" OR ")
 			}
 			}
 			if strings.HasPrefix(v, "not_") {
 			if strings.HasPrefix(v, "not_") {
-				sql.WriteString("state <> ? ")
+				builder.Write("state <> ? ")
 				v = strings.TrimPrefix(v, "not_")
 				v = strings.TrimPrefix(v, "not_")
 			} else {
 			} else {
-				sql.WriteString("state = ? ")
+				builder.Write("state = ? ")
 			}
 			}
-			params = append(params, v)
+			builder.AddParams(v)
 		}
 		}
-		sql.WriteString(")")
+		builder.Write(")")
 	}
 	}
 
 
-	sql.WriteString(" ORDER BY name ASC")
+	if query.User.OrgRole != m.ROLE_ADMIN {
+		builder.writeDashboardPermissionFilter(query.User, m.PERMISSION_EDIT)
+	}
+
+	builder.Write(" ORDER BY name ASC")
 
 
 	if query.Limit != 0 {
 	if query.Limit != 0 {
-		sql.WriteString(" LIMIT ?")
-		params = append(params, query.Limit)
+		builder.Write(" LIMIT ?", query.Limit)
 	}
 	}
 
 
-	alerts := make([]*m.Alert, 0)
-	if err := x.SQL(sql.String(), params...).Find(&alerts); err != nil {
+	alerts := make([]*m.AlertListItemDTO, 0)
+	if err := x.SQL(builder.GetSqlString(), builder.params...).Find(&alerts); err != nil {
 		return err
 		return err
 	}
 	}
 
 

+ 13 - 7
pkg/services/sqlstore/alert_test.go

@@ -71,15 +71,21 @@ func TestAlertingDataAccess(t *testing.T) {
 		})
 		})
 
 
 		Convey("Can read properties", func() {
 		Convey("Can read properties", func() {
-			alertQuery := m.GetAlertsQuery{DashboardId: testDash.Id, PanelId: 1, OrgId: 1}
+			alertQuery := m.GetAlertsQuery{DashboardId: testDash.Id, PanelId: 1, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}}
 			err2 := HandleAlertsQuery(&alertQuery)
 			err2 := HandleAlertsQuery(&alertQuery)
 
 
 			alert := alertQuery.Result[0]
 			alert := alertQuery.Result[0]
 			So(err2, ShouldBeNil)
 			So(err2, ShouldBeNil)
 			So(alert.Name, ShouldEqual, "Alerting title")
 			So(alert.Name, ShouldEqual, "Alerting title")
-			So(alert.Message, ShouldEqual, "Alerting message")
 			So(alert.State, ShouldEqual, "pending")
 			So(alert.State, ShouldEqual, "pending")
-			So(alert.Frequency, ShouldEqual, 1)
+		})
+
+		Convey("Viewer cannot read alerts", func() {
+			alertQuery := m.GetAlertsQuery{DashboardId: testDash.Id, PanelId: 1, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_VIEWER}}
+			err2 := HandleAlertsQuery(&alertQuery)
+
+			So(err2, ShouldBeNil)
+			So(alertQuery.Result, ShouldHaveLength, 0)
 		})
 		})
 
 
 		Convey("Alerts with same dashboard id and panel id should update", func() {
 		Convey("Alerts with same dashboard id and panel id should update", func() {
@@ -100,7 +106,7 @@ func TestAlertingDataAccess(t *testing.T) {
 			})
 			})
 
 
 			Convey("Alerts should be updated", func() {
 			Convey("Alerts should be updated", func() {
-				query := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1}
+				query := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}}
 				err2 := HandleAlertsQuery(&query)
 				err2 := HandleAlertsQuery(&query)
 
 
 				So(err2, ShouldBeNil)
 				So(err2, ShouldBeNil)
@@ -149,7 +155,7 @@ func TestAlertingDataAccess(t *testing.T) {
 			Convey("Should save 3 dashboards", func() {
 			Convey("Should save 3 dashboards", func() {
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
 
 
-				queryForDashboard := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1}
+				queryForDashboard := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}}
 				err2 := HandleAlertsQuery(&queryForDashboard)
 				err2 := HandleAlertsQuery(&queryForDashboard)
 
 
 				So(err2, ShouldBeNil)
 				So(err2, ShouldBeNil)
@@ -163,7 +169,7 @@ func TestAlertingDataAccess(t *testing.T) {
 				err = SaveAlerts(&cmd)
 				err = SaveAlerts(&cmd)
 
 
 				Convey("should delete the missing alert", func() {
 				Convey("should delete the missing alert", func() {
-					query := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1}
+					query := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}}
 					err2 := HandleAlertsQuery(&query)
 					err2 := HandleAlertsQuery(&query)
 					So(err2, ShouldBeNil)
 					So(err2, ShouldBeNil)
 					So(len(query.Result), ShouldEqual, 2)
 					So(len(query.Result), ShouldEqual, 2)
@@ -198,7 +204,7 @@ func TestAlertingDataAccess(t *testing.T) {
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 
 
 			Convey("Alerts should be removed", func() {
 			Convey("Alerts should be removed", func() {
-				query := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1}
+				query := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}}
 				err2 := HandleAlertsQuery(&query)
 				err2 := HandleAlertsQuery(&query)
 
 
 				So(testDash.Id, ShouldEqual, 1)
 				So(testDash.Id, ShouldEqual, 1)

+ 199 - 221
pkg/services/sqlstore/dashboard.go

@@ -21,195 +21,120 @@ func init() {
 	bus.AddHandler("sql", GetDashboardSlugById)
 	bus.AddHandler("sql", GetDashboardSlugById)
 	bus.AddHandler("sql", GetDashboardUIDById)
 	bus.AddHandler("sql", GetDashboardUIDById)
 	bus.AddHandler("sql", GetDashboardsByPluginId)
 	bus.AddHandler("sql", GetDashboardsByPluginId)
-	bus.AddHandler("sql", GetFoldersForSignedInUser)
 	bus.AddHandler("sql", GetDashboardPermissionsForUser)
 	bus.AddHandler("sql", GetDashboardPermissionsForUser)
 	bus.AddHandler("sql", GetDashboardsBySlug)
 	bus.AddHandler("sql", GetDashboardsBySlug)
+	bus.AddHandler("sql", ValidateDashboardBeforeSave)
 }
 }
 
 
 var generateNewUid func() string = util.GenerateShortUid
 var generateNewUid func() string = util.GenerateShortUid
 
 
 func SaveDashboard(cmd *m.SaveDashboardCommand) error {
 func SaveDashboard(cmd *m.SaveDashboardCommand) error {
 	return inTransaction(func(sess *DBSession) error {
 	return inTransaction(func(sess *DBSession) error {
-		dash := cmd.GetDashboardModel()
-
-		if err := getExistingDashboardForUpdate(sess, dash, cmd); err != nil {
-			return err
-		}
-
-		var existingByTitleAndFolder m.Dashboard
-
-		dashWithTitleAndFolderExists, err := sess.Where("org_id=? AND slug=? AND (is_folder=? OR folder_id=?)", dash.OrgId, dash.Slug, dialect.BooleanStr(true), dash.FolderId).Get(&existingByTitleAndFolder)
-		if err != nil {
-			return err
-		}
-
-		if dashWithTitleAndFolderExists {
-			if dash.Id != existingByTitleAndFolder.Id {
-				if existingByTitleAndFolder.IsFolder && !cmd.IsFolder {
-					return m.ErrDashboardWithSameNameAsFolder
-				}
-
-				if !existingByTitleAndFolder.IsFolder && cmd.IsFolder {
-					return m.ErrDashboardFolderWithSameNameAsDashboard
-				}
-
-				if cmd.Overwrite {
-					dash.Id = existingByTitleAndFolder.Id
-					dash.Version = existingByTitleAndFolder.Version
-
-					if dash.Uid == "" {
-						dash.Uid = existingByTitleAndFolder.Uid
-					}
-				} else {
-					return m.ErrDashboardWithSameNameInFolderExists
-				}
-			}
-		}
-
-		if dash.Uid == "" {
-			uid, err := generateNewDashboardUid(sess, dash.OrgId)
-			if err != nil {
-				return err
-			}
-			dash.Uid = uid
-			dash.Data.Set("uid", uid)
-		}
-
-		err = setHasAcl(sess, dash)
-		if err != nil {
-			return err
-		}
-
-		parentVersion := dash.Version
-		affectedRows := int64(0)
-
-		if dash.Id == 0 {
-			dash.Version = 1
-			metrics.M_Api_Dashboard_Insert.Inc()
-			dash.Data.Set("version", dash.Version)
-			affectedRows, err = sess.Insert(dash)
-		} else {
-			dash.Version++
-			dash.Data.Set("version", dash.Version)
-
-			if !cmd.UpdatedAt.IsZero() {
-				dash.Updated = cmd.UpdatedAt
-			}
+		return saveDashboard(sess, cmd)
+	})
+}
 
 
-			affectedRows, err = sess.MustCols("folder_id", "has_acl").ID(dash.Id).Update(dash)
-		}
+func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error {
+	dash := cmd.GetDashboardModel()
 
 
+	if dash.Id > 0 {
+		var existing m.Dashboard
+		dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existing)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
-
-		if affectedRows == 0 {
+		if !dashWithIdExists {
 			return m.ErrDashboardNotFound
 			return m.ErrDashboardNotFound
 		}
 		}
 
 
-		dashVersion := &m.DashboardVersion{
-			DashboardId:   dash.Id,
-			ParentVersion: parentVersion,
-			RestoredFrom:  cmd.RestoredFrom,
-			Version:       dash.Version,
-			Created:       time.Now(),
-			CreatedBy:     dash.UpdatedBy,
-			Message:       cmd.Message,
-			Data:          dash.Data,
+		// check for is someone else has written in between
+		if dash.Version != existing.Version {
+			if cmd.Overwrite {
+				dash.SetVersion(existing.Version)
+			} else {
+				return m.ErrDashboardVersionMismatch
+			}
 		}
 		}
 
 
-		// insert version entry
-		if affectedRows, err = sess.Insert(dashVersion); err != nil {
-			return err
-		} else if affectedRows == 0 {
-			return m.ErrDashboardNotFound
+		// do not allow plugin dashboard updates without overwrite flag
+		if existing.PluginId != "" && cmd.Overwrite == false {
+			return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
 		}
 		}
+	}
 
 
-		// delete existing tags
-		_, err = sess.Exec("DELETE FROM dashboard_tag WHERE dashboard_id=?", dash.Id)
+	if dash.Uid == "" {
+		uid, err := generateNewDashboardUid(sess, dash.OrgId)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
+		dash.SetUid(uid)
+	}
 
 
-		// insert new tags
-		tags := dash.GetTags()
-		if len(tags) > 0 {
-			for _, tag := range tags {
-				if _, err := sess.Insert(&DashboardTag{DashboardId: dash.Id, Term: tag}); err != nil {
-					return err
-				}
-			}
-		}
-		cmd.Result = dash
-
-		return err
-	})
-}
-
-func getExistingDashboardForUpdate(sess *DBSession, dash *m.Dashboard, cmd *m.SaveDashboardCommand) (err error) {
-	dashWithIdExists := false
-	var existingById m.Dashboard
+	parentVersion := dash.Version
+	affectedRows := int64(0)
+	var err error
 
 
-	if dash.Id > 0 {
-		dashWithIdExists, err = sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existingById)
-		if err != nil {
-			return err
-		}
+	if dash.Id == 0 {
+		dash.SetVersion(1)
+		metrics.M_Api_Dashboard_Insert.Inc()
+		affectedRows, err = sess.Insert(dash)
+	} else {
+		v := dash.Version
+		v++
+		dash.SetVersion(v)
 
 
-		if !dashWithIdExists {
-			return m.ErrDashboardNotFound
+		if !cmd.UpdatedAt.IsZero() {
+			dash.Updated = cmd.UpdatedAt
 		}
 		}
 
 
-		if dash.Uid == "" {
-			dash.Uid = existingById.Uid
-		}
+		affectedRows, err = sess.MustCols("folder_id").ID(dash.Id).Update(dash)
 	}
 	}
 
 
-	dashWithUidExists := false
-	var existingByUid m.Dashboard
-
-	if dash.Uid != "" {
-		dashWithUidExists, err = sess.Where("org_id=? AND uid=?", dash.OrgId, dash.Uid).Get(&existingByUid)
-		if err != nil {
-			return err
-		}
+	if err != nil {
+		return err
 	}
 	}
 
 
-	if !dashWithIdExists && !dashWithUidExists {
-		return nil
+	if affectedRows == 0 {
+		return m.ErrDashboardNotFound
 	}
 	}
 
 
-	if dashWithIdExists && dashWithUidExists && existingById.Id != existingByUid.Id {
-		return m.ErrDashboardWithSameUIDExists
+	dashVersion := &m.DashboardVersion{
+		DashboardId:   dash.Id,
+		ParentVersion: parentVersion,
+		RestoredFrom:  cmd.RestoredFrom,
+		Version:       dash.Version,
+		Created:       time.Now(),
+		CreatedBy:     dash.UpdatedBy,
+		Message:       cmd.Message,
+		Data:          dash.Data,
 	}
 	}
 
 
-	existing := existingById
-
-	if !dashWithIdExists && dashWithUidExists {
-		dash.Id = existingByUid.Id
-		existing = existingByUid
+	// insert version entry
+	if affectedRows, err = sess.Insert(dashVersion); err != nil {
+		return err
+	} else if affectedRows == 0 {
+		return m.ErrDashboardNotFound
 	}
 	}
 
 
-	if (existing.IsFolder && !cmd.IsFolder) ||
-		(!existing.IsFolder && cmd.IsFolder) {
-		return m.ErrDashboardTypeMismatch
+	// delete existing tags
+	_, err = sess.Exec("DELETE FROM dashboard_tag WHERE dashboard_id=?", dash.Id)
+	if err != nil {
+		return err
 	}
 	}
 
 
-	// check for is someone else has written in between
-	if dash.Version != existing.Version {
-		if cmd.Overwrite {
-			dash.Version = existing.Version
-		} else {
-			return m.ErrDashboardVersionMismatch
+	// insert new tags
+	tags := dash.GetTags()
+	if len(tags) > 0 {
+		for _, tag := range tags {
+			if _, err := sess.Insert(&DashboardTag{DashboardId: dash.Id, Term: tag}); err != nil {
+				return err
+			}
 		}
 		}
 	}
 	}
 
 
-	// do not allow plugin dashboard updates without overwrite flag
-	if existing.PluginId != "" && cmd.Overwrite == false {
-		return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
-	}
+	cmd.Result = dash
 
 
-	return nil
+	return err
 }
 }
 
 
 func generateNewDashboardUid(sess *DBSession, orgId int64) (string, error) {
 func generateNewDashboardUid(sess *DBSession, orgId int64) (string, error) {
@@ -229,31 +154,6 @@ func generateNewDashboardUid(sess *DBSession, orgId int64) (string, error) {
 	return "", m.ErrDashboardFailedGenerateUniqueUid
 	return "", m.ErrDashboardFailedGenerateUniqueUid
 }
 }
 
 
-func setHasAcl(sess *DBSession, dash *m.Dashboard) error {
-	// check if parent has acl
-	if dash.FolderId > 0 {
-		var parent m.Dashboard
-		if hasParent, err := sess.Where("folder_id=?", dash.FolderId).Get(&parent); err != nil {
-			return err
-		} else if hasParent && parent.HasAcl {
-			dash.HasAcl = true
-		}
-	}
-
-	// check if dash has its own acl
-	if dash.Id > 0 {
-		if res, err := sess.Query("SELECT 1 from dashboard_acl WHERE dashboard_id =?", dash.Id); err != nil {
-			return err
-		} else {
-			if len(res) > 0 {
-				dash.HasAcl = true
-			}
-		}
-	}
-
-	return nil
-}
-
 func GetDashboard(query *m.GetDashboardQuery) error {
 func GetDashboard(query *m.GetDashboardQuery) error {
 	dashboard := m.Dashboard{Slug: query.Slug, OrgId: query.OrgId, Id: query.Id, Uid: query.Uid}
 	dashboard := m.Dashboard{Slug: query.Slug, OrgId: query.OrgId, Id: query.Id, Uid: query.Uid}
 	has, err := x.Get(&dashboard)
 	has, err := x.Get(&dashboard)
@@ -264,8 +164,8 @@ func GetDashboard(query *m.GetDashboardQuery) error {
 		return m.ErrDashboardNotFound
 		return m.ErrDashboardNotFound
 	}
 	}
 
 
-	dashboard.Data.Set("id", dashboard.Id)
-	dashboard.Data.Set("uid", dashboard.Uid)
+	dashboard.SetId(dashboard.Id)
+	dashboard.SetUid(dashboard.Uid)
 	query.Result = &dashboard
 	query.Result = &dashboard
 	return nil
 	return nil
 }
 }
@@ -289,7 +189,7 @@ func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSear
 		limit = 1000
 		limit = 1000
 	}
 	}
 
 
-	sb := NewSearchBuilder(query.SignedInUser, limit).
+	sb := NewSearchBuilder(query.SignedInUser, limit, query.Permission).
 		WithTags(query.Tags).
 		WithTags(query.Tags).
 		WithDashboardIdsIn(query.DashboardIds)
 		WithDashboardIdsIn(query.DashboardIds)
 
 
@@ -390,54 +290,6 @@ func GetDashboardTags(query *m.GetDashboardTagsQuery) error {
 	return err
 	return err
 }
 }
 
 
-func GetFoldersForSignedInUser(query *m.GetFoldersForSignedInUserQuery) error {
-	query.Result = make([]*m.DashboardFolder, 0)
-	var err error
-
-	if query.SignedInUser.OrgRole == m.ROLE_ADMIN {
-		sql := `SELECT distinct d.id, d.title
-		FROM dashboard AS d WHERE d.is_folder = ? AND d.org_id = ?
-		ORDER BY d.title ASC`
-
-		err = x.Sql(sql, dialect.BooleanStr(true), query.OrgId).Find(&query.Result)
-	} else {
-		params := make([]interface{}, 0)
-		sql := `SELECT distinct d.id, d.title
-		FROM dashboard AS d
-			LEFT JOIN dashboard_acl AS da ON d.id = da.dashboard_id
-			LEFT JOIN team_member AS ugm ON ugm.team_id =  da.team_id
-			LEFT JOIN org_user ou ON ou.role = da.role AND ou.user_id = ?
-			LEFT JOIN org_user ouRole ON ouRole.role = 'Editor' AND ouRole.user_id = ? AND ouRole.org_id = ?`
-		params = append(params, query.SignedInUser.UserId)
-		params = append(params, query.SignedInUser.UserId)
-		params = append(params, query.OrgId)
-
-		sql += ` WHERE
-			d.org_id = ? AND
-			d.is_folder = ? AND
-			(
-				(d.has_acl = ? AND da.permission > 1 AND (da.user_id = ? OR ugm.user_id = ? OR ou.id IS NOT NULL))
-				OR (d.has_acl = ? AND ouRole.id IS NOT NULL)
-			)`
-		params = append(params, query.OrgId)
-		params = append(params, dialect.BooleanStr(true))
-		params = append(params, dialect.BooleanStr(true))
-		params = append(params, query.SignedInUser.UserId)
-		params = append(params, query.SignedInUser.UserId)
-		params = append(params, dialect.BooleanStr(false))
-
-		if len(query.Title) > 0 {
-			sql += " AND d.title " + dialect.LikeStr() + " ?"
-			params = append(params, "%"+query.Title+"%")
-		}
-
-		sql += ` ORDER BY d.title ASC`
-		err = x.Sql(sql, params...).Find(&query.Result)
-	}
-
-	return err
-}
-
 func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
 func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
 	return inTransaction(func(sess *DBSession) error {
 	return inTransaction(func(sess *DBSession) error {
 		dashboard := m.Dashboard{Id: cmd.Id, OrgId: cmd.OrgId}
 		dashboard := m.Dashboard{Id: cmd.Id, OrgId: cmd.OrgId}
@@ -456,6 +308,7 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
 			"DELETE FROM dashboard_version WHERE dashboard_id = ?",
 			"DELETE FROM dashboard_version WHERE dashboard_id = ?",
 			"DELETE FROM dashboard WHERE folder_id = ?",
 			"DELETE FROM dashboard WHERE folder_id = ?",
 			"DELETE FROM annotation WHERE dashboard_id = ?",
 			"DELETE FROM annotation WHERE dashboard_id = ?",
+			"DELETE FROM dashboard_provisioning WHERE dashboard_id = ?",
 		}
 		}
 
 
 		for _, sql := range deletes {
 		for _, sql := range deletes {
@@ -621,3 +474,128 @@ func GetDashboardUIDById(query *m.GetDashboardRefByIdQuery) error {
 	query.Result = us
 	query.Result = us
 	return nil
 	return nil
 }
 }
+
+func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *m.ValidateDashboardBeforeSaveCommand) (err error) {
+	dash := cmd.Dashboard
+
+	dashWithIdExists := false
+	var existingById m.Dashboard
+
+	if dash.Id > 0 {
+		dashWithIdExists, err = sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existingById)
+		if err != nil {
+			return err
+		}
+
+		if !dashWithIdExists {
+			return m.ErrDashboardNotFound
+		}
+
+		if dash.Uid == "" {
+			dash.SetUid(existingById.Uid)
+		}
+	}
+
+	dashWithUidExists := false
+	var existingByUid m.Dashboard
+
+	if dash.Uid != "" {
+		dashWithUidExists, err = sess.Where("org_id=? AND uid=?", dash.OrgId, dash.Uid).Get(&existingByUid)
+		if err != nil {
+			return err
+		}
+	}
+
+	if dash.FolderId > 0 {
+		var existingFolder m.Dashboard
+		folderExists, folderErr := sess.Where("org_id=? AND id=? AND is_folder=?", dash.OrgId, dash.FolderId, dialect.BooleanStr(true)).Get(&existingFolder)
+		if folderErr != nil {
+			return folderErr
+		}
+
+		if !folderExists {
+			return m.ErrFolderNotFound
+		}
+	}
+
+	if !dashWithIdExists && !dashWithUidExists {
+		return nil
+	}
+
+	if dashWithIdExists && dashWithUidExists && existingById.Id != existingByUid.Id {
+		return m.ErrDashboardWithSameUIDExists
+	}
+
+	existing := existingById
+
+	if !dashWithIdExists && dashWithUidExists {
+		dash.SetId(existingByUid.Id)
+		dash.SetUid(existingByUid.Uid)
+		existing = existingByUid
+	}
+
+	if (existing.IsFolder && !dash.IsFolder) ||
+		(!existing.IsFolder && dash.IsFolder) {
+		return m.ErrDashboardTypeMismatch
+	}
+
+	// check for is someone else has written in between
+	if dash.Version != existing.Version {
+		if cmd.Overwrite {
+			dash.SetVersion(existing.Version)
+		} else {
+			return m.ErrDashboardVersionMismatch
+		}
+	}
+
+	// do not allow plugin dashboard updates without overwrite flag
+	if existing.PluginId != "" && cmd.Overwrite == false {
+		return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
+	}
+
+	return nil
+}
+
+func getExistingDashboardByTitleAndFolder(sess *DBSession, cmd *m.ValidateDashboardBeforeSaveCommand) error {
+	dash := cmd.Dashboard
+	var existing m.Dashboard
+
+	exists, err := sess.Where("org_id=? AND slug=? AND (is_folder=? OR folder_id=?)", dash.OrgId, dash.Slug, dialect.BooleanStr(true), dash.FolderId).Get(&existing)
+	if err != nil {
+		return err
+	}
+
+	if exists && dash.Id != existing.Id {
+		if existing.IsFolder && !dash.IsFolder {
+			return m.ErrDashboardWithSameNameAsFolder
+		}
+
+		if !existing.IsFolder && dash.IsFolder {
+			return m.ErrDashboardFolderWithSameNameAsDashboard
+		}
+
+		if cmd.Overwrite {
+			dash.SetId(existing.Id)
+			dash.SetUid(existing.Uid)
+			dash.SetVersion(existing.Version)
+		} else {
+			return m.ErrDashboardWithSameNameInFolderExists
+		}
+	}
+
+	return nil
+}
+
+func ValidateDashboardBeforeSave(cmd *m.ValidateDashboardBeforeSaveCommand) (err error) {
+	return inTransaction(func(sess *DBSession) error {
+		if err = getExistingDashboardByIdOrUidForUpdate(sess, cmd); err != nil {
+			return err
+		}
+
+		if err = getExistingDashboardByTitleAndFolder(sess, cmd); err != nil {
+			return err
+		}
+
+		return nil
+	})
+}

+ 20 - 127
pkg/services/sqlstore/dashboard_acl.go

@@ -1,17 +1,12 @@
 package sqlstore
 package sqlstore
 
 
 import (
 import (
-	"fmt"
-	"time"
-
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
 )
 )
 
 
 func init() {
 func init() {
-	bus.AddHandler("sql", SetDashboardAcl)
 	bus.AddHandler("sql", UpdateDashboardAcl)
 	bus.AddHandler("sql", UpdateDashboardAcl)
-	bus.AddHandler("sql", RemoveDashboardAcl)
 	bus.AddHandler("sql", GetDashboardAclInfoList)
 	bus.AddHandler("sql", GetDashboardAclInfoList)
 }
 }
 
 
@@ -24,7 +19,7 @@ func UpdateDashboardAcl(cmd *m.UpdateDashboardAclCommand) error {
 		}
 		}
 
 
 		for _, item := range cmd.Items {
 		for _, item := range cmd.Items {
-			if item.UserId == 0 && item.TeamId == 0 && !item.Role.IsValid() {
+			if item.UserId == 0 && item.TeamId == 0 && (item.Role == nil || !item.Role.IsValid()) {
 				return m.ErrDashboardAclInfoMissing
 				return m.ErrDashboardAclInfoMissing
 			}
 			}
 
 
@@ -40,92 +35,13 @@ func UpdateDashboardAcl(cmd *m.UpdateDashboardAclCommand) error {
 
 
 		// Update dashboard HasAcl flag
 		// Update dashboard HasAcl flag
 		dashboard := m.Dashboard{HasAcl: true}
 		dashboard := m.Dashboard{HasAcl: true}
-		if _, err := sess.Cols("has_acl").Where("id=? OR folder_id=?", cmd.DashboardId, cmd.DashboardId).Update(&dashboard); err != nil {
-			return err
-		}
-		return nil
-	})
-}
-
-func SetDashboardAcl(cmd *m.SetDashboardAclCommand) error {
-	return inTransaction(func(sess *DBSession) error {
-		if cmd.UserId == 0 && cmd.TeamId == 0 {
-			return m.ErrDashboardAclInfoMissing
-		}
-
-		if cmd.DashboardId == 0 {
-			return m.ErrDashboardPermissionDashboardEmpty
-		}
-
-		if res, err := sess.Query("SELECT 1 from "+dialect.Quote("dashboard_acl")+" WHERE dashboard_id =? and (team_id=? or user_id=?)", cmd.DashboardId, cmd.TeamId, cmd.UserId); err != nil {
-			return err
-		} else if len(res) == 1 {
-
-			entity := m.DashboardAcl{
-				Permission: cmd.Permission,
-				Updated:    time.Now(),
-			}
-
-			if _, err := sess.Cols("updated", "permission").Where("dashboard_id =? and (team_id=? or user_id=?)", cmd.DashboardId, cmd.TeamId, cmd.UserId).Update(&entity); err != nil {
-				return err
-			}
-
-			return nil
-		}
-
-		entity := m.DashboardAcl{
-			OrgId:       cmd.OrgId,
-			TeamId:      cmd.TeamId,
-			UserId:      cmd.UserId,
-			Created:     time.Now(),
-			Updated:     time.Now(),
-			DashboardId: cmd.DashboardId,
-			Permission:  cmd.Permission,
-		}
-
-		cols := []string{"org_id", "created", "updated", "dashboard_id", "permission"}
-
-		if cmd.UserId != 0 {
-			cols = append(cols, "user_id")
-		}
-
-		if cmd.TeamId != 0 {
-			cols = append(cols, "team_id")
-		}
-
-		_, err := sess.Cols(cols...).Insert(&entity)
-		if err != nil {
-			return err
-		}
-
-		cmd.Result = entity
-
-		// Update dashboard HasAcl flag
-		dashboard := m.Dashboard{
-			HasAcl: true,
-		}
-
-		if _, err := sess.Cols("has_acl").Where("id=? OR folder_id=?", cmd.DashboardId, cmd.DashboardId).Update(&dashboard); err != nil {
+		if _, err := sess.Cols("has_acl").Where("id=?", cmd.DashboardId).Update(&dashboard); err != nil {
 			return err
 			return err
 		}
 		}
-
 		return nil
 		return nil
 	})
 	})
 }
 }
 
 
-// RemoveDashboardAcl removes a specified permission from the dashboard acl
-func RemoveDashboardAcl(cmd *m.RemoveDashboardAclCommand) error {
-	return inTransaction(func(sess *DBSession) error {
-		var rawSQL = "DELETE FROM " + dialect.Quote("dashboard_acl") + " WHERE org_id =? and id=?"
-		_, err := sess.Exec(rawSQL, cmd.OrgId, cmd.AclId)
-		if err != nil {
-			return err
-		}
-
-		return err
-	})
-}
-
 // GetDashboardAclInfoList returns a list of permissions for a dashboard. They can be fetched from three
 // GetDashboardAclInfoList returns a list of permissions for a dashboard. They can be fetched from three
 // different places.
 // different places.
 // 1) Permissions for the dashboard
 // 1) Permissions for the dashboard
@@ -134,6 +50,8 @@ func RemoveDashboardAcl(cmd *m.RemoveDashboardAclCommand) error {
 func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
 func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
 	var err error
 	var err error
 
 
+	falseStr := dialect.BooleanStr(false)
+
 	if query.DashboardId == 0 {
 	if query.DashboardId == 0 {
 		sql := `SELECT
 		sql := `SELECT
 		da.id,
 		da.id,
@@ -151,18 +69,13 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
 		'' as title,
 		'' as title,
 		'' as slug,
 		'' as slug,
 		'' as uid,` +
 		'' as uid,` +
-			dialect.BooleanStr(false) + ` AS is_folder
+			falseStr + ` AS is_folder
 		FROM dashboard_acl as da
 		FROM dashboard_acl as da
 		WHERE da.dashboard_id = -1`
 		WHERE da.dashboard_id = -1`
 		query.Result = make([]*m.DashboardAclInfoDTO, 0)
 		query.Result = make([]*m.DashboardAclInfoDTO, 0)
 		err = x.SQL(sql).Find(&query.Result)
 		err = x.SQL(sql).Find(&query.Result)
 
 
 	} else {
 	} else {
-		dashboardFilter := fmt.Sprintf(`IN (
-			SELECT %d
-			UNION
-			SELECT folder_id from dashboard where id = %d
-		  )`, query.DashboardId, query.DashboardId)
 
 
 		rawSQL := `
 		rawSQL := `
 			-- get permissions for the dashboard and its parent folder
 			-- get permissions for the dashboard and its parent folder
@@ -183,41 +96,21 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
 				d.slug,
 				d.slug,
 				d.uid,
 				d.uid,
 				d.is_folder
 				d.is_folder
-		  FROM` + dialect.Quote("dashboard_acl") + ` as da
-				LEFT OUTER JOIN ` + dialect.Quote("user") + ` AS u ON u.id = da.user_id
-				LEFT OUTER JOIN team ug on ug.id = da.team_id
-				LEFT OUTER JOIN dashboard d on da.dashboard_id = d.id
-			WHERE dashboard_id ` + dashboardFilter + ` AND da.org_id = ?
-
-			-- Also include default permissions if folder or dashboard field "has_acl" is false
-
-			UNION
-				SELECT
-					da.id,
-					da.org_id,
-					da.dashboard_id,
-					da.user_id,
-					da.team_id,
-					da.permission,
-					da.role,
-					da.created,
-					da.updated,
-					'' as user_login,
-					'' as user_email,
-					'' as team,
-					folder.title,
-					folder.slug,
-					folder.uid,
-					folder.is_folder
-				FROM dashboard_acl as da,
-				dashboard as dash
-				LEFT OUTER JOIN dashboard folder on dash.folder_id = folder.id
-					WHERE
-						dash.id = ? AND (
-							dash.has_acl = ` + dialect.BooleanStr(false) + ` or
-							folder.has_acl = ` + dialect.BooleanStr(false) + `
-						) AND
-						da.dashboard_id = -1
+			FROM dashboard as d
+				LEFT JOIN dashboard folder on folder.id = d.folder_id
+				LEFT JOIN dashboard_acl AS da ON
+				da.dashboard_id = d.id OR
+				da.dashboard_id = d.folder_id OR
+				(
+					-- include default permissions -->
+					da.org_id = -1 AND (
+					  (folder.id IS NOT NULL AND folder.has_acl = ` + falseStr + `) OR
+					  (folder.id IS NULL AND d.has_acl = ` + falseStr + `)
+					)
+				)
+				LEFT JOIN ` + dialect.Quote("user") + ` AS u ON u.id = da.user_id
+				LEFT JOIN team ug on ug.id = da.team_id
+			WHERE d.org_id = ? AND d.id = ? AND da.id IS NOT NULL
 			ORDER BY 1 ASC
 			ORDER BY 1 ASC
 			`
 			`
 
 

+ 27 - 64
pkg/services/sqlstore/dashboard_acl_test.go

@@ -17,7 +17,7 @@ func TestDashboardAclDataAccess(t *testing.T) {
 			childDash := insertTestDashboard("2 test dash", 1, savedFolder.Id, false, "prod", "webapp")
 			childDash := insertTestDashboard("2 test dash", 1, savedFolder.Id, false, "prod", "webapp")
 
 
 			Convey("When adding dashboard permission with userId and teamId set to 0", func() {
 			Convey("When adding dashboard permission with userId and teamId set to 0", func() {
-				err := SetDashboardAcl(&m.SetDashboardAclCommand{
+				err := testHelperUpdateDashboardAcl(savedFolder.Id, m.DashboardAcl{
 					OrgId:       1,
 					OrgId:       1,
 					DashboardId: savedFolder.Id,
 					DashboardId: savedFolder.Id,
 					Permission:  m.PERMISSION_EDIT,
 					Permission:  m.PERMISSION_EDIT,
@@ -41,8 +41,25 @@ func TestDashboardAclDataAccess(t *testing.T) {
 				})
 				})
 			})
 			})
 
 
+			Convey("Given dashboard folder with removed default permissions", func() {
+				err := UpdateDashboardAcl(&m.UpdateDashboardAclCommand{
+					DashboardId: savedFolder.Id,
+					Items:       []*m.DashboardAcl{},
+				})
+				So(err, ShouldBeNil)
+
+				Convey("When reading dashboard acl should return no acl items", func() {
+					query := m.GetDashboardAclInfoListQuery{DashboardId: childDash.Id, OrgId: 1}
+
+					err := GetDashboardAclInfoList(&query)
+					So(err, ShouldBeNil)
+
+					So(len(query.Result), ShouldEqual, 0)
+				})
+			})
+
 			Convey("Given dashboard folder permission", func() {
 			Convey("Given dashboard folder permission", func() {
-				err := SetDashboardAcl(&m.SetDashboardAclCommand{
+				err := testHelperUpdateDashboardAcl(savedFolder.Id, m.DashboardAcl{
 					OrgId:       1,
 					OrgId:       1,
 					UserId:      currentUser.Id,
 					UserId:      currentUser.Id,
 					DashboardId: savedFolder.Id,
 					DashboardId: savedFolder.Id,
@@ -61,7 +78,7 @@ func TestDashboardAclDataAccess(t *testing.T) {
 				})
 				})
 
 
 				Convey("Given child dashboard permission", func() {
 				Convey("Given child dashboard permission", func() {
-					err := SetDashboardAcl(&m.SetDashboardAclCommand{
+					err := testHelperUpdateDashboardAcl(childDash.Id, m.DashboardAcl{
 						OrgId:       1,
 						OrgId:       1,
 						UserId:      currentUser.Id,
 						UserId:      currentUser.Id,
 						DashboardId: childDash.Id,
 						DashboardId: childDash.Id,
@@ -83,7 +100,7 @@ func TestDashboardAclDataAccess(t *testing.T) {
 			})
 			})
 
 
 			Convey("Given child dashboard permission in folder with no permissions", func() {
 			Convey("Given child dashboard permission in folder with no permissions", func() {
-				err := SetDashboardAcl(&m.SetDashboardAclCommand{
+				err := testHelperUpdateDashboardAcl(childDash.Id, m.DashboardAcl{
 					OrgId:       1,
 					OrgId:       1,
 					UserId:      currentUser.Id,
 					UserId:      currentUser.Id,
 					DashboardId: childDash.Id,
 					DashboardId: childDash.Id,
@@ -108,17 +125,12 @@ func TestDashboardAclDataAccess(t *testing.T) {
 			})
 			})
 
 
 			Convey("Should be able to add dashboard permission", func() {
 			Convey("Should be able to add dashboard permission", func() {
-				setDashAclCmd := m.SetDashboardAclCommand{
+				err := testHelperUpdateDashboardAcl(savedFolder.Id, m.DashboardAcl{
 					OrgId:       1,
 					OrgId:       1,
 					UserId:      currentUser.Id,
 					UserId:      currentUser.Id,
 					DashboardId: savedFolder.Id,
 					DashboardId: savedFolder.Id,
 					Permission:  m.PERMISSION_EDIT,
 					Permission:  m.PERMISSION_EDIT,
-				}
-
-				err := SetDashboardAcl(&setDashAclCmd)
-				So(err, ShouldBeNil)
-
-				So(setDashAclCmd.Result.Id, ShouldEqual, 3)
+				})
 
 
 				q1 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
 				q1 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
 				err = GetDashboardAclInfoList(q1)
 				err = GetDashboardAclInfoList(q1)
@@ -130,42 +142,9 @@ func TestDashboardAclDataAccess(t *testing.T) {
 				So(q1.Result[0].UserId, ShouldEqual, currentUser.Id)
 				So(q1.Result[0].UserId, ShouldEqual, currentUser.Id)
 				So(q1.Result[0].UserLogin, ShouldEqual, currentUser.Login)
 				So(q1.Result[0].UserLogin, ShouldEqual, currentUser.Login)
 				So(q1.Result[0].UserEmail, ShouldEqual, currentUser.Email)
 				So(q1.Result[0].UserEmail, ShouldEqual, currentUser.Email)
-				So(q1.Result[0].Id, ShouldEqual, setDashAclCmd.Result.Id)
-
-				Convey("Should update hasAcl field to true for dashboard folder and its children", func() {
-					q2 := &m.GetDashboardsQuery{DashboardIds: []int64{savedFolder.Id, childDash.Id}}
-					err := GetDashboards(q2)
-					So(err, ShouldBeNil)
-					So(q2.Result[0].HasAcl, ShouldBeTrue)
-					So(q2.Result[1].HasAcl, ShouldBeTrue)
-				})
-
-				Convey("Should be able to update an existing permission", func() {
-					err := SetDashboardAcl(&m.SetDashboardAclCommand{
-						OrgId:       1,
-						UserId:      1,
-						DashboardId: savedFolder.Id,
-						Permission:  m.PERMISSION_ADMIN,
-					})
-
-					So(err, ShouldBeNil)
-
-					q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
-					err = GetDashboardAclInfoList(q3)
-					So(err, ShouldBeNil)
-					So(len(q3.Result), ShouldEqual, 1)
-					So(q3.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
-					So(q3.Result[0].Permission, ShouldEqual, m.PERMISSION_ADMIN)
-					So(q3.Result[0].UserId, ShouldEqual, 1)
-
-				})
 
 
 				Convey("Should be able to delete an existing permission", func() {
 				Convey("Should be able to delete an existing permission", func() {
-					err := RemoveDashboardAcl(&m.RemoveDashboardAclCommand{
-						OrgId: 1,
-						AclId: setDashAclCmd.Result.Id,
-					})
-
+					err := testHelperUpdateDashboardAcl(savedFolder.Id)
 					So(err, ShouldBeNil)
 					So(err, ShouldBeNil)
 
 
 					q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
 					q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
@@ -181,14 +160,12 @@ func TestDashboardAclDataAccess(t *testing.T) {
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
 
 
 				Convey("Should be able to add a user permission for a team", func() {
 				Convey("Should be able to add a user permission for a team", func() {
-					setDashAclCmd := m.SetDashboardAclCommand{
+					err := testHelperUpdateDashboardAcl(savedFolder.Id, m.DashboardAcl{
 						OrgId:       1,
 						OrgId:       1,
 						TeamId:      group1.Result.Id,
 						TeamId:      group1.Result.Id,
 						DashboardId: savedFolder.Id,
 						DashboardId: savedFolder.Id,
 						Permission:  m.PERMISSION_EDIT,
 						Permission:  m.PERMISSION_EDIT,
-					}
-
-					err := SetDashboardAcl(&setDashAclCmd)
+					})
 					So(err, ShouldBeNil)
 					So(err, ShouldBeNil)
 
 
 					q1 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
 					q1 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
@@ -197,23 +174,10 @@ func TestDashboardAclDataAccess(t *testing.T) {
 					So(q1.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
 					So(q1.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
 					So(q1.Result[0].Permission, ShouldEqual, m.PERMISSION_EDIT)
 					So(q1.Result[0].Permission, ShouldEqual, m.PERMISSION_EDIT)
 					So(q1.Result[0].TeamId, ShouldEqual, group1.Result.Id)
 					So(q1.Result[0].TeamId, ShouldEqual, group1.Result.Id)
-
-					Convey("Should be able to delete an existing permission for a team", func() {
-						err := RemoveDashboardAcl(&m.RemoveDashboardAclCommand{
-							OrgId: 1,
-							AclId: setDashAclCmd.Result.Id,
-						})
-
-						So(err, ShouldBeNil)
-						q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
-						err = GetDashboardAclInfoList(q3)
-						So(err, ShouldBeNil)
-						So(len(q3.Result), ShouldEqual, 0)
-					})
 				})
 				})
 
 
 				Convey("Should be able to update an existing permission for a team", func() {
 				Convey("Should be able to update an existing permission for a team", func() {
-					err := SetDashboardAcl(&m.SetDashboardAclCommand{
+					err := testHelperUpdateDashboardAcl(savedFolder.Id, m.DashboardAcl{
 						OrgId:       1,
 						OrgId:       1,
 						TeamId:      group1.Result.Id,
 						TeamId:      group1.Result.Id,
 						DashboardId: savedFolder.Id,
 						DashboardId: savedFolder.Id,
@@ -229,7 +193,6 @@ func TestDashboardAclDataAccess(t *testing.T) {
 					So(q3.Result[0].Permission, ShouldEqual, m.PERMISSION_ADMIN)
 					So(q3.Result[0].Permission, ShouldEqual, m.PERMISSION_ADMIN)
 					So(q3.Result[0].TeamId, ShouldEqual, group1.Result.Id)
 					So(q3.Result[0].TeamId, ShouldEqual, group1.Result.Id)
 				})
 				})
-
 			})
 			})
 		})
 		})
 
 

+ 51 - 40
pkg/services/sqlstore/dashboard_folder_test.go

@@ -26,7 +26,11 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 
 
 			Convey("and no acls are set", func() {
 			Convey("and no acls are set", func() {
 				Convey("should return all dashboards", func() {
 				Convey("should return all dashboards", func() {
-					query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}}
+					query := &search.FindPersistedDashboardsQuery{
+						SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER},
+						OrgId:        1,
+						DashboardIds: []int64{folder.Id, dashInRoot.Id},
+					}
 					err := SearchDashboards(query)
 					err := SearchDashboards(query)
 					So(err, ShouldBeNil)
 					So(err, ShouldBeNil)
 					So(len(query.Result), ShouldEqual, 2)
 					So(len(query.Result), ShouldEqual, 2)
@@ -37,10 +41,13 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 
 
 			Convey("and acl is set for dashboard folder", func() {
 			Convey("and acl is set for dashboard folder", func() {
 				var otherUser int64 = 999
 				var otherUser int64 = 999
-				updateTestDashboardWithAcl(folder.Id, otherUser, m.PERMISSION_EDIT)
+				testHelperUpdateDashboardAcl(folder.Id, m.DashboardAcl{DashboardId: folder.Id, OrgId: 1, UserId: otherUser, Permission: m.PERMISSION_EDIT})
 
 
 				Convey("should not return folder", func() {
 				Convey("should not return folder", func() {
-					query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}}
+					query := &search.FindPersistedDashboardsQuery{
+						SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER},
+						OrgId:        1, DashboardIds: []int64{folder.Id, dashInRoot.Id},
+					}
 					err := SearchDashboards(query)
 					err := SearchDashboards(query)
 					So(err, ShouldBeNil)
 					So(err, ShouldBeNil)
 					So(len(query.Result), ShouldEqual, 1)
 					So(len(query.Result), ShouldEqual, 1)
@@ -48,10 +55,14 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 				})
 				})
 
 
 				Convey("when the user is given permission", func() {
 				Convey("when the user is given permission", func() {
-					updateTestDashboardWithAcl(folder.Id, currentUser.Id, m.PERMISSION_EDIT)
+					testHelperUpdateDashboardAcl(folder.Id, m.DashboardAcl{DashboardId: folder.Id, OrgId: 1, UserId: currentUser.Id, Permission: m.PERMISSION_EDIT})
 
 
 					Convey("should be able to access folder", func() {
 					Convey("should be able to access folder", func() {
-						query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}}
+						query := &search.FindPersistedDashboardsQuery{
+							SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER},
+							OrgId:        1,
+							DashboardIds: []int64{folder.Id, dashInRoot.Id},
+						}
 						err := SearchDashboards(query)
 						err := SearchDashboards(query)
 						So(err, ShouldBeNil)
 						So(err, ShouldBeNil)
 						So(len(query.Result), ShouldEqual, 2)
 						So(len(query.Result), ShouldEqual, 2)
@@ -82,12 +93,11 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 
 
 			Convey("and acl is set for dashboard child and folder has all permissions removed", func() {
 			Convey("and acl is set for dashboard child and folder has all permissions removed", func() {
 				var otherUser int64 = 999
 				var otherUser int64 = 999
-				aclId := updateTestDashboardWithAcl(folder.Id, otherUser, m.PERMISSION_EDIT)
-				removeAcl(aclId)
-				updateTestDashboardWithAcl(childDash.Id, otherUser, m.PERMISSION_EDIT)
+				testHelperUpdateDashboardAcl(folder.Id)
+				testHelperUpdateDashboardAcl(childDash.Id, m.DashboardAcl{DashboardId: folder.Id, OrgId: 1, UserId: otherUser, Permission: m.PERMISSION_EDIT})
 
 
 				Convey("should not return folder or child", func() {
 				Convey("should not return folder or child", func() {
-					query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}}
+					query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}}
 					err := SearchDashboards(query)
 					err := SearchDashboards(query)
 					So(err, ShouldBeNil)
 					So(err, ShouldBeNil)
 					So(len(query.Result), ShouldEqual, 1)
 					So(len(query.Result), ShouldEqual, 1)
@@ -95,10 +105,10 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 				})
 				})
 
 
 				Convey("when the user is given permission to child", func() {
 				Convey("when the user is given permission to child", func() {
-					updateTestDashboardWithAcl(childDash.Id, currentUser.Id, m.PERMISSION_EDIT)
+					testHelperUpdateDashboardAcl(childDash.Id, m.DashboardAcl{DashboardId: childDash.Id, OrgId: 1, UserId: currentUser.Id, Permission: m.PERMISSION_EDIT})
 
 
 					Convey("should be able to search for child dashboard but not folder", func() {
 					Convey("should be able to search for child dashboard but not folder", func() {
-						query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}}
+						query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}}
 						err := SearchDashboards(query)
 						err := SearchDashboards(query)
 						So(err, ShouldBeNil)
 						So(err, ShouldBeNil)
 						So(len(query.Result), ShouldEqual, 2)
 						So(len(query.Result), ShouldEqual, 2)
@@ -141,7 +151,7 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 
 
 			Convey("and one folder is expanded, the other collapsed", func() {
 			Convey("and one folder is expanded, the other collapsed", func() {
 				Convey("should return dashboards in root and expanded folder", func() {
 				Convey("should return dashboards in root and expanded folder", func() {
-					query := &search.FindPersistedDashboardsQuery{FolderIds: []int64{rootFolderId, folder1.Id}, SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1}
+					query := &search.FindPersistedDashboardsQuery{FolderIds: []int64{rootFolderId, folder1.Id}, SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER}, OrgId: 1}
 					err := SearchDashboards(query)
 					err := SearchDashboards(query)
 					So(err, ShouldBeNil)
 					So(err, ShouldBeNil)
 					So(len(query.Result), ShouldEqual, 4)
 					So(len(query.Result), ShouldEqual, 4)
@@ -154,15 +164,14 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 
 
 			Convey("and acl is set for one dashboard folder", func() {
 			Convey("and acl is set for one dashboard folder", func() {
 				var otherUser int64 = 999
 				var otherUser int64 = 999
-				updateTestDashboardWithAcl(folder1.Id, otherUser, m.PERMISSION_EDIT)
+				testHelperUpdateDashboardAcl(folder1.Id, m.DashboardAcl{DashboardId: folder1.Id, OrgId: 1, UserId: otherUser, Permission: m.PERMISSION_EDIT})
 
 
 				Convey("and a dashboard is moved from folder without acl to the folder with an acl", func() {
 				Convey("and a dashboard is moved from folder without acl to the folder with an acl", func() {
-					movedDash := moveDashboard(1, childDash2.Data, folder1.Id)
-					So(movedDash.HasAcl, ShouldBeTrue)
+					moveDashboard(1, childDash2.Data, folder1.Id)
 
 
 					Convey("should not return folder with acl or its children", func() {
 					Convey("should not return folder with acl or its children", func() {
 						query := &search.FindPersistedDashboardsQuery{
 						query := &search.FindPersistedDashboardsQuery{
-							SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1},
+							SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER},
 							OrgId:        1,
 							OrgId:        1,
 							DashboardIds: []int64{folder1.Id, childDash1.Id, childDash2.Id, dashInRoot.Id},
 							DashboardIds: []int64{folder1.Id, childDash1.Id, childDash2.Id, dashInRoot.Id},
 						}
 						}
@@ -172,14 +181,12 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 						So(query.Result[0].Id, ShouldEqual, dashInRoot.Id)
 						So(query.Result[0].Id, ShouldEqual, dashInRoot.Id)
 					})
 					})
 				})
 				})
-
 				Convey("and a dashboard is moved from folder with acl to the folder without an acl", func() {
 				Convey("and a dashboard is moved from folder with acl to the folder without an acl", func() {
-					movedDash := moveDashboard(1, childDash1.Data, folder2.Id)
-					So(movedDash.HasAcl, ShouldBeFalse)
+					moveDashboard(1, childDash1.Data, folder2.Id)
 
 
 					Convey("should return folder without acl and its children", func() {
 					Convey("should return folder without acl and its children", func() {
 						query := &search.FindPersistedDashboardsQuery{
 						query := &search.FindPersistedDashboardsQuery{
-							SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1},
+							SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER},
 							OrgId:        1,
 							OrgId:        1,
 							DashboardIds: []int64{folder2.Id, childDash1.Id, childDash2.Id, dashInRoot.Id},
 							DashboardIds: []int64{folder2.Id, childDash1.Id, childDash2.Id, dashInRoot.Id},
 						}
 						}
@@ -194,22 +201,22 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 				})
 				})
 
 
 				Convey("and a dashboard with an acl is moved to the folder without an acl", func() {
 				Convey("and a dashboard with an acl is moved to the folder without an acl", func() {
-					updateTestDashboardWithAcl(childDash1.Id, otherUser, m.PERMISSION_EDIT)
-					movedDash := moveDashboard(1, childDash1.Data, folder2.Id)
-					So(movedDash.HasAcl, ShouldBeTrue)
+					testHelperUpdateDashboardAcl(childDash1.Id, m.DashboardAcl{DashboardId: childDash1.Id, OrgId: 1, UserId: otherUser, Permission: m.PERMISSION_EDIT})
+					moveDashboard(1, childDash1.Data, folder2.Id)
 
 
 					Convey("should return folder without acl but not the dashboard with acl", func() {
 					Convey("should return folder without acl but not the dashboard with acl", func() {
 						query := &search.FindPersistedDashboardsQuery{
 						query := &search.FindPersistedDashboardsQuery{
-							SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1},
+							SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER},
 							OrgId:        1,
 							OrgId:        1,
 							DashboardIds: []int64{folder2.Id, childDash1.Id, childDash2.Id, dashInRoot.Id},
 							DashboardIds: []int64{folder2.Id, childDash1.Id, childDash2.Id, dashInRoot.Id},
 						}
 						}
 						err := SearchDashboards(query)
 						err := SearchDashboards(query)
 						So(err, ShouldBeNil)
 						So(err, ShouldBeNil)
-						So(len(query.Result), ShouldEqual, 3)
+						So(len(query.Result), ShouldEqual, 4)
 						So(query.Result[0].Id, ShouldEqual, folder2.Id)
 						So(query.Result[0].Id, ShouldEqual, folder2.Id)
-						So(query.Result[1].Id, ShouldEqual, childDash2.Id)
-						So(query.Result[2].Id, ShouldEqual, dashInRoot.Id)
+						So(query.Result[1].Id, ShouldEqual, childDash1.Id)
+						So(query.Result[2].Id, ShouldEqual, childDash2.Id)
+						So(query.Result[3].Id, ShouldEqual, dashInRoot.Id)
 					})
 					})
 				})
 				})
 			})
 			})
@@ -227,12 +234,14 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 
 
 			Convey("Admin users", func() {
 			Convey("Admin users", func() {
 				Convey("Should have write access to all dashboard folders in their org", func() {
 				Convey("Should have write access to all dashboard folders in their org", func() {
-					query := m.GetFoldersForSignedInUserQuery{
+					query := search.FindPersistedDashboardsQuery{
 						OrgId:        1,
 						OrgId:        1,
-						SignedInUser: &m.SignedInUser{UserId: adminUser.Id, OrgRole: m.ROLE_ADMIN},
+						SignedInUser: &m.SignedInUser{UserId: adminUser.Id, OrgRole: m.ROLE_ADMIN, OrgId: 1},
+						Permission:   m.PERMISSION_VIEW,
+						Type:         "dash-folder",
 					}
 					}
 
 
-					err := GetFoldersForSignedInUser(&query)
+					err := SearchDashboards(&query)
 					So(err, ShouldBeNil)
 					So(err, ShouldBeNil)
 
 
 					So(len(query.Result), ShouldEqual, 2)
 					So(len(query.Result), ShouldEqual, 2)
@@ -260,13 +269,14 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 			})
 			})
 
 
 			Convey("Editor users", func() {
 			Convey("Editor users", func() {
-				query := m.GetFoldersForSignedInUserQuery{
+				query := search.FindPersistedDashboardsQuery{
 					OrgId:        1,
 					OrgId:        1,
-					SignedInUser: &m.SignedInUser{UserId: editorUser.Id, OrgRole: m.ROLE_EDITOR},
+					SignedInUser: &m.SignedInUser{UserId: editorUser.Id, OrgRole: m.ROLE_EDITOR, OrgId: 1},
+					Permission:   m.PERMISSION_EDIT,
 				}
 				}
 
 
 				Convey("Should have write access to all dashboard folders with default ACL", func() {
 				Convey("Should have write access to all dashboard folders with default ACL", func() {
-					err := GetFoldersForSignedInUser(&query)
+					err := SearchDashboards(&query)
 					So(err, ShouldBeNil)
 					So(err, ShouldBeNil)
 
 
 					So(len(query.Result), ShouldEqual, 2)
 					So(len(query.Result), ShouldEqual, 2)
@@ -293,9 +303,9 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 				})
 				})
 
 
 				Convey("Should have write access to one dashboard folder if default role changed to view for one folder", func() {
 				Convey("Should have write access to one dashboard folder if default role changed to view for one folder", func() {
-					updateTestDashboardWithAcl(folder1.Id, editorUser.Id, m.PERMISSION_VIEW)
+					testHelperUpdateDashboardAcl(folder1.Id, m.DashboardAcl{DashboardId: folder1.Id, OrgId: 1, UserId: editorUser.Id, Permission: m.PERMISSION_VIEW})
 
 
-					err := GetFoldersForSignedInUser(&query)
+					err := SearchDashboards(&query)
 					So(err, ShouldBeNil)
 					So(err, ShouldBeNil)
 
 
 					So(len(query.Result), ShouldEqual, 1)
 					So(len(query.Result), ShouldEqual, 1)
@@ -305,13 +315,14 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 			})
 			})
 
 
 			Convey("Viewer users", func() {
 			Convey("Viewer users", func() {
-				query := m.GetFoldersForSignedInUserQuery{
+				query := search.FindPersistedDashboardsQuery{
 					OrgId:        1,
 					OrgId:        1,
-					SignedInUser: &m.SignedInUser{UserId: viewerUser.Id, OrgRole: m.ROLE_VIEWER},
+					SignedInUser: &m.SignedInUser{UserId: viewerUser.Id, OrgRole: m.ROLE_VIEWER, OrgId: 1},
+					Permission:   m.PERMISSION_EDIT,
 				}
 				}
 
 
 				Convey("Should have no write access to any dashboard folders with default ACL", func() {
 				Convey("Should have no write access to any dashboard folders with default ACL", func() {
-					err := GetFoldersForSignedInUser(&query)
+					err := SearchDashboards(&query)
 					So(err, ShouldBeNil)
 					So(err, ShouldBeNil)
 
 
 					So(len(query.Result), ShouldEqual, 0)
 					So(len(query.Result), ShouldEqual, 0)
@@ -336,9 +347,9 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 				})
 				})
 
 
 				Convey("Should be able to get one dashboard folder if default role changed to edit for one folder", func() {
 				Convey("Should be able to get one dashboard folder if default role changed to edit for one folder", func() {
-					updateTestDashboardWithAcl(folder1.Id, viewerUser.Id, m.PERMISSION_EDIT)
+					testHelperUpdateDashboardAcl(folder1.Id, m.DashboardAcl{DashboardId: folder1.Id, OrgId: 1, UserId: viewerUser.Id, Permission: m.PERMISSION_EDIT})
 
 
-					err := GetFoldersForSignedInUser(&query)
+					err := SearchDashboards(&query)
 					So(err, ShouldBeNil)
 					So(err, ShouldBeNil)
 
 
 					So(len(query.Result), ShouldEqual, 1)
 					So(len(query.Result), ShouldEqual, 1)

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

@@ -0,0 +1,66 @@
+package sqlstore
+
+import (
+	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/models"
+)
+
+func init() {
+	bus.AddHandler("sql", GetProvisionedDashboardDataQuery)
+	bus.AddHandler("sql", SaveProvisionedDashboard)
+}
+
+type DashboardExtras struct {
+	Id          int64
+	DashboardId int64
+	Key         string
+	Value       string
+}
+
+func SaveProvisionedDashboard(cmd *models.SaveProvisionedDashboardCommand) error {
+	return inTransaction(func(sess *DBSession) error {
+		err := saveDashboard(sess, cmd.DashboardCmd)
+
+		if err != nil {
+			return err
+		}
+
+		cmd.Result = cmd.DashboardCmd.Result
+		if cmd.DashboardProvisioning.Updated == 0 {
+			cmd.DashboardProvisioning.Updated = cmd.Result.Updated.Unix()
+		}
+
+		return saveProvionedData(sess, cmd.DashboardProvisioning, cmd.Result)
+	})
+}
+
+func saveProvionedData(sess *DBSession, cmd *models.DashboardProvisioning, dashboard *models.Dashboard) error {
+	result := &models.DashboardProvisioning{}
+
+	exist, err := sess.Where("dashboard_id=?", dashboard.Id).Get(result)
+	if err != nil {
+		return err
+	}
+
+	cmd.Id = result.Id
+	cmd.DashboardId = dashboard.Id
+
+	if exist {
+		_, err = sess.ID(result.Id).Update(cmd)
+	} else {
+		_, err = sess.Insert(cmd)
+	}
+
+	return err
+}
+
+func GetProvisionedDashboardDataQuery(cmd *models.GetProvisionedDashboardDataQuery) error {
+	var result []*models.DashboardProvisioning
+
+	if err := x.Where("name = ?", cmd.Name).Find(&result); err != nil {
+		return err
+	}
+
+	cmd.Result = result
+	return nil
+}

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

@@ -0,0 +1,55 @@
+package sqlstore
+
+import (
+	"testing"
+	"time"
+
+	"github.com/grafana/grafana/pkg/components/simplejson"
+	"github.com/grafana/grafana/pkg/models"
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestDashboardProvisioningTest(t *testing.T) {
+	Convey("Testing Dashboard provisioning", t, func() {
+		InitTestDB(t)
+
+		saveDashboardCmd := &models.SaveDashboardCommand{
+			OrgId:    1,
+			FolderId: 0,
+			IsFolder: false,
+			Dashboard: simplejson.NewFromAny(map[string]interface{}{
+				"id":    nil,
+				"title": "test dashboard",
+			}),
+		}
+
+		Convey("Saving dashboards with extras", func() {
+			now := time.Now()
+
+			cmd := &models.SaveProvisionedDashboardCommand{
+				DashboardCmd: saveDashboardCmd,
+				DashboardProvisioning: &models.DashboardProvisioning{
+					Name:       "default",
+					ExternalId: "/var/grafana.json",
+					Updated:    now.Unix(),
+				},
+			}
+
+			err := SaveProvisionedDashboard(cmd)
+			So(err, ShouldBeNil)
+			So(cmd.Result, ShouldNotBeNil)
+			So(cmd.Result.Id, ShouldNotEqual, 0)
+			dashId := cmd.Result.Id
+
+			Convey("Can query for provisioned dashboards", func() {
+				query := &models.GetProvisionedDashboardDataQuery{Name: "default"}
+				err := GetProvisionedDashboardDataQuery(query)
+				So(err, ShouldBeNil)
+
+				So(len(query.Result), ShouldEqual, 1)
+				So(query.Result[0].DashboardId, ShouldEqual, dashId)
+				So(query.Result[0].Updated, ShouldEqual, now.Unix())
+			})
+		})
+	})
+}

+ 984 - 0
pkg/services/sqlstore/dashboard_service_integration_test.go

@@ -0,0 +1,984 @@
+package sqlstore
+
+import (
+	"testing"
+
+	"github.com/grafana/grafana/pkg/components/simplejson"
+	"github.com/grafana/grafana/pkg/services/dashboards"
+	"github.com/grafana/grafana/pkg/services/guardian"
+
+	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/models"
+
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestIntegratedDashboardService(t *testing.T) {
+	Convey("Dashboard service integration tests", t, func() {
+		InitTestDB(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
+			})
+
+			bus.AddHandler("test", func(cmd *models.UpdateDashboardAlertsCommand) error {
+				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)
+			savedDashInGeneralFolder := saveTestDashboard("Saved dashboard in general folder", testOrgId, 0)
+			otherSavedFolder := saveTestFolder("Other saved folder", testOrgId)
+
+			Convey("Should return dashboard model", func() {
+				So(savedFolder.Title, ShouldEqual, "Saved folder")
+				So(savedFolder.Slug, ShouldEqual, "saved-folder")
+				So(savedFolder.Id, ShouldNotEqual, 0)
+				So(savedFolder.IsFolder, ShouldBeTrue)
+				So(savedFolder.FolderId, ShouldEqual, 0)
+				So(len(savedFolder.Uid), ShouldBeGreaterThan, 0)
+
+				So(savedDashInFolder.Title, ShouldEqual, "Saved dash in folder")
+				So(savedDashInFolder.Slug, ShouldEqual, "saved-dash-in-folder")
+				So(savedDashInFolder.Id, ShouldNotEqual, 0)
+				So(savedDashInFolder.IsFolder, ShouldBeFalse)
+				So(savedDashInFolder.FolderId, ShouldEqual, savedFolder.Id)
+				So(len(savedDashInFolder.Uid), ShouldBeGreaterThan, 0)
+			})
+
+			// Basic validation tests
+
+			Convey("When saving a dashboard with non-existing id", func() {
+				cmd := models.SaveDashboardCommand{
+					OrgId: testOrgId,
+					Dashboard: simplejson.NewFromAny(map[string]interface{}{
+						"id":    float64(123412321),
+						"title": "Expect error",
+					}),
+				}
+
+				err := callSaveWithError(cmd)
+
+				Convey("It should result in not found error", func() {
+					So(err, ShouldNotBeNil)
+					So(err, ShouldEqual, models.ErrDashboardNotFound)
+				})
+			})
+
+			// Given other organization
+
+			Convey("Given organization B", func() {
+				var otherOrgId int64 = 2
+
+				Convey("When saving a dashboard with id that are saved in organization A", func() {
+					cmd := models.SaveDashboardCommand{
+						OrgId: otherOrgId,
+						Dashboard: simplejson.NewFromAny(map[string]interface{}{
+							"id":    savedDashInFolder.Id,
+							"title": "Expect error",
+						}),
+						Overwrite: false,
+					}
+
+					err := callSaveWithError(cmd)
+
+					Convey("It should result in not found error", func() {
+						So(err, ShouldNotBeNil)
+						So(err, ShouldEqual, models.ErrDashboardNotFound)
+					})
+				})
+
+				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() {
+						var otherOrgId int64 = 2
+						cmd := models.SaveDashboardCommand{
+							OrgId: otherOrgId,
+							Dashboard: simplejson.NewFromAny(map[string]interface{}{
+								"uid":   savedDashInFolder.Uid,
+								"title": "Dash with existing uid in other org",
+							}),
+							Overwrite: false,
+						}
+
+						res := callSaveWithResult(cmd)
+
+						Convey("It should create dashboard in other organization", func() {
+							So(res, ShouldNotBeNil)
+
+							query := models.GetDashboardQuery{OrgId: otherOrgId, Uid: savedDashInFolder.Uid}
+
+							err := bus.Dispatch(&query)
+							So(err, ShouldBeNil)
+							So(query.Result.Id, ShouldNotEqual, savedDashInFolder.Id)
+							So(query.Result.Id, ShouldEqual, res.Id)
+							So(query.Result.OrgId, ShouldEqual, otherOrgId)
+							So(query.Result.Uid, ShouldEqual, savedDashInFolder.Uid)
+						})
+					})
+				})
+			})
+
+			// Given user has no permission to save
+
+			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() {
+					cmd := models.SaveDashboardCommand{
+						OrgId: testOrgId,
+						Dashboard: simplejson.NewFromAny(map[string]interface{}{
+							"title": "Dash",
+						}),
+						UserId:    10000,
+						Overwrite: true,
+					}
+
+					err := callSaveWithError(cmd)
+
+					Convey("It should call dashboard guardian 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 trying to create a new dashboard in other folder", func() {
+					cmd := models.SaveDashboardCommand{
+						OrgId: testOrgId,
+						Dashboard: simplejson.NewFromAny(map[string]interface{}{
+							"title": "Dash",
+						}),
+						FolderId:  otherSavedFolder.Id,
+						UserId:    10000,
+						Overwrite: true,
+					}
+
+					err := callSaveWithError(cmd)
+
+					Convey("It should call dashboard guardian with correct arguments and rsult 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 trying to update a dashboard by existing id in the General folder", func() {
+					cmd := models.SaveDashboardCommand{
+						OrgId: testOrgId,
+						Dashboard: simplejson.NewFromAny(map[string]interface{}{
+							"id":    savedDashInGeneralFolder.Id,
+							"title": "Dash",
+						}),
+						FolderId:  savedDashInGeneralFolder.FolderId,
+						UserId:    10000,
+						Overwrite: true,
+					}
+
+					err := callSaveWithError(cmd)
+
+					Convey("It should call dashboard guardian with correct arguments and result in access denied error", func() {
+						So(err, ShouldNotBeNil)
+						So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
+
+						So(sc.dashboardGuardianMock.dashId, ShouldEqual, savedDashInGeneralFolder.Id)
+						So(sc.dashboardGuardianMock.orgId, ShouldEqual, cmd.OrgId)
+						So(sc.dashboardGuardianMock.user.UserId, ShouldEqual, cmd.UserId)
+					})
+				})
+
+				Convey("When trying to update a dashboard by existing id in other folder", func() {
+					cmd := models.SaveDashboardCommand{
+						OrgId: testOrgId,
+						Dashboard: simplejson.NewFromAny(map[string]interface{}{
+							"id":    savedDashInFolder.Id,
+							"title": "Dash",
+						}),
+						FolderId:  savedDashInFolder.FolderId,
+						UserId:    10000,
+						Overwrite: true,
+					}
+
+					err := callSaveWithError(cmd)
+
+					Convey("It should call dashboard guardian with correct arguments and result in access denied error", func() {
+						So(err, ShouldNotBeNil)
+						So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
+
+						So(sc.dashboardGuardianMock.dashId, ShouldEqual, savedDashInFolder.Id)
+						So(sc.dashboardGuardianMock.orgId, ShouldEqual, cmd.OrgId)
+						So(sc.dashboardGuardianMock.user.UserId, ShouldEqual, cmd.UserId)
+					})
+				})
+			})
+
+			// Given user has permission to save
+
+			permissionScenario("Given user has permission to save", true, func(sc *dashboardPermissionScenarioContext) {
+
+				Convey("and overwrite flag is set to false", func() {
+					shouldOverwrite := false
+
+					Convey("When creating a dashboard in General folder with same name as dashboard in other folder", func() {
+						cmd := models.SaveDashboardCommand{
+							OrgId: testOrgId,
+							Dashboard: simplejson.NewFromAny(map[string]interface{}{
+								"id":    nil,
+								"title": savedDashInFolder.Title,
+							}),
+							FolderId:  0,
+							Overwrite: shouldOverwrite,
+						}
+
+						res := callSaveWithResult(cmd)
+						So(res, ShouldNotBeNil)
+
+						Convey("It should create a new dashboard", func() {
+							query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: res.Id}
+
+							err := bus.Dispatch(&query)
+							So(err, ShouldBeNil)
+							So(query.Result.Id, ShouldEqual, res.Id)
+							So(query.Result.FolderId, ShouldEqual, 0)
+						})
+					})
+
+					Convey("When creating a dashboard in other folder with same name as dashboard in General folder", func() {
+						cmd := models.SaveDashboardCommand{
+							OrgId: testOrgId,
+							Dashboard: simplejson.NewFromAny(map[string]interface{}{
+								"id":    nil,
+								"title": savedDashInGeneralFolder.Title,
+							}),
+							FolderId:  savedFolder.Id,
+							Overwrite: shouldOverwrite,
+						}
+
+						res := callSaveWithResult(cmd)
+						So(res, ShouldNotBeNil)
+
+						Convey("It should create a new dashboard", func() {
+							So(res.Id, ShouldNotEqual, savedDashInGeneralFolder.Id)
+
+							query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: res.Id}
+
+							err := bus.Dispatch(&query)
+							So(err, ShouldBeNil)
+							So(query.Result.FolderId, ShouldEqual, savedFolder.Id)
+						})
+					})
+
+					Convey("When creating a folder with same name as dashboard in other folder", func() {
+						cmd := models.SaveDashboardCommand{
+							OrgId: testOrgId,
+							Dashboard: simplejson.NewFromAny(map[string]interface{}{
+								"id":    nil,
+								"title": savedDashInFolder.Title,
+							}),
+							IsFolder:  true,
+							Overwrite: shouldOverwrite,
+						}
+
+						res := callSaveWithResult(cmd)
+						So(res, ShouldNotBeNil)
+
+						Convey("It should create a new folder", func() {
+							So(res.Id, ShouldNotEqual, savedDashInGeneralFolder.Id)
+							So(res.IsFolder, ShouldBeTrue)
+
+							query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: res.Id}
+
+							err := bus.Dispatch(&query)
+							So(err, ShouldBeNil)
+							So(query.Result.FolderId, ShouldEqual, 0)
+							So(query.Result.IsFolder, ShouldBeTrue)
+						})
+					})
+
+					Convey("When saving a dashboard without id and uid and unique title in folder", func() {
+						cmd := models.SaveDashboardCommand{
+							OrgId: testOrgId,
+							Dashboard: simplejson.NewFromAny(map[string]interface{}{
+								"title": "Dash without id and uid",
+							}),
+							Overwrite: shouldOverwrite,
+						}
+
+						res := callSaveWithResult(cmd)
+						So(res, ShouldNotBeNil)
+
+						Convey("It should create a new dashboard", func() {
+							So(res.Id, ShouldBeGreaterThan, 0)
+							So(len(res.Uid), ShouldBeGreaterThan, 0)
+							query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: res.Id}
+
+							err := bus.Dispatch(&query)
+							So(err, ShouldBeNil)
+							So(query.Result.Id, ShouldEqual, res.Id)
+							So(query.Result.Uid, ShouldEqual, res.Uid)
+						})
+					})
+
+					Convey("When saving a dashboard when dashboard id is zero ", func() {
+						cmd := models.SaveDashboardCommand{
+							OrgId: testOrgId,
+							Dashboard: simplejson.NewFromAny(map[string]interface{}{
+								"id":    0,
+								"title": "Dash with zero id",
+							}),
+							Overwrite: shouldOverwrite,
+						}
+
+						res := callSaveWithResult(cmd)
+						So(res, ShouldNotBeNil)
+
+						Convey("It should create a new dashboard", func() {
+							query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: res.Id}
+
+							err := bus.Dispatch(&query)
+							So(err, ShouldBeNil)
+							So(query.Result.Id, ShouldEqual, res.Id)
+						})
+					})
+
+					Convey("When saving a dashboard in non-existing folder", func() {
+						cmd := models.SaveDashboardCommand{
+							OrgId: testOrgId,
+							Dashboard: simplejson.NewFromAny(map[string]interface{}{
+								"title": "Expect error",
+							}),
+							FolderId:  123412321,
+							Overwrite: shouldOverwrite,
+						}
+
+						err := callSaveWithError(cmd)
+
+						Convey("It should result in folder not found error", func() {
+							So(err, ShouldNotBeNil)
+							So(err, ShouldEqual, models.ErrFolderNotFound)
+						})
+					})
+
+					Convey("When updating an existing dashboard by id without current version", func() {
+						cmd := models.SaveDashboardCommand{
+							OrgId: 1,
+							Dashboard: simplejson.NewFromAny(map[string]interface{}{
+								"id":    savedDashInGeneralFolder.Id,
+								"title": "test dash 23",
+							}),
+							FolderId:  savedFolder.Id,
+							Overwrite: shouldOverwrite,
+						}
+
+						err := callSaveWithError(cmd)
+
+						Convey("It should result in version mismatch error", func() {
+							So(err, ShouldNotBeNil)
+							So(err, ShouldEqual, models.ErrDashboardVersionMismatch)
+						})
+					})
+
+					Convey("When updating an existing dashboard by id with current version", func() {
+						cmd := models.SaveDashboardCommand{
+							OrgId: 1,
+							Dashboard: simplejson.NewFromAny(map[string]interface{}{
+								"id":      savedDashInGeneralFolder.Id,
+								"title":   "Updated title",
+								"version": savedDashInGeneralFolder.Version,
+							}),
+							FolderId:  savedFolder.Id,
+							Overwrite: shouldOverwrite,
+						}
+
+						res := callSaveWithResult(cmd)
+						So(res, ShouldNotBeNil)
+
+						Convey("It should update dashboard", func() {
+							query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: savedDashInGeneralFolder.Id}
+
+							err := bus.Dispatch(&query)
+							So(err, ShouldBeNil)
+							So(query.Result.Title, ShouldEqual, "Updated title")
+							So(query.Result.FolderId, ShouldEqual, savedFolder.Id)
+							So(query.Result.Version, ShouldBeGreaterThan, savedDashInGeneralFolder.Version)
+						})
+					})
+
+					Convey("When updating an existing dashboard by uid without current version", func() {
+						cmd := models.SaveDashboardCommand{
+							OrgId: 1,
+							Dashboard: simplejson.NewFromAny(map[string]interface{}{
+								"uid":   savedDashInFolder.Uid,
+								"title": "test dash 23",
+							}),
+							FolderId:  0,
+							Overwrite: shouldOverwrite,
+						}
+
+						err := callSaveWithError(cmd)
+
+						Convey("It should result in version mismatch error", func() {
+							So(err, ShouldNotBeNil)
+							So(err, ShouldEqual, models.ErrDashboardVersionMismatch)
+						})
+					})
+
+					Convey("When updating an existing dashboard by uid with current version", func() {
+						cmd := models.SaveDashboardCommand{
+							OrgId: 1,
+							Dashboard: simplejson.NewFromAny(map[string]interface{}{
+								"uid":     savedDashInFolder.Uid,
+								"title":   "Updated title",
+								"version": savedDashInFolder.Version,
+							}),
+							FolderId:  0,
+							Overwrite: shouldOverwrite,
+						}
+
+						res := callSaveWithResult(cmd)
+						So(res, ShouldNotBeNil)
+
+						Convey("It should update dashboard", func() {
+							query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: savedDashInFolder.Id}
+
+							err := bus.Dispatch(&query)
+							So(err, ShouldBeNil)
+							So(query.Result.Title, ShouldEqual, "Updated title")
+							So(query.Result.FolderId, ShouldEqual, 0)
+							So(query.Result.Version, ShouldBeGreaterThan, savedDashInFolder.Version)
+						})
+					})
+
+					Convey("When creating a dashboard with same name as dashboard in other folder", func() {
+						cmd := models.SaveDashboardCommand{
+							OrgId: testOrgId,
+							Dashboard: simplejson.NewFromAny(map[string]interface{}{
+								"id":    nil,
+								"title": savedDashInFolder.Title,
+							}),
+							FolderId:  savedDashInFolder.FolderId,
+							Overwrite: shouldOverwrite,
+						}
+
+						err := callSaveWithError(cmd)
+
+						Convey("It should result in dashboard with same name in folder error", func() {
+							So(err, ShouldNotBeNil)
+							So(err, ShouldEqual, models.ErrDashboardWithSameNameInFolderExists)
+						})
+					})
+
+					Convey("When creating a dashboard with same name as dashboard in General folder", func() {
+						cmd := models.SaveDashboardCommand{
+							OrgId: testOrgId,
+							Dashboard: simplejson.NewFromAny(map[string]interface{}{
+								"id":    nil,
+								"title": savedDashInGeneralFolder.Title,
+							}),
+							FolderId:  savedDashInGeneralFolder.FolderId,
+							Overwrite: shouldOverwrite,
+						}
+
+						err := callSaveWithError(cmd)
+
+						Convey("It should result in dashboard with same name in folder error", func() {
+							So(err, ShouldNotBeNil)
+							So(err, ShouldEqual, models.ErrDashboardWithSameNameInFolderExists)
+						})
+					})
+
+					Convey("When creating a folder with same name as existing folder", func() {
+						cmd := models.SaveDashboardCommand{
+							OrgId: testOrgId,
+							Dashboard: simplejson.NewFromAny(map[string]interface{}{
+								"id":    nil,
+								"title": savedFolder.Title,
+							}),
+							IsFolder:  true,
+							Overwrite: shouldOverwrite,
+						}
+
+						err := callSaveWithError(cmd)
+
+						Convey("It should result in dashboard with same name in folder error", func() {
+							So(err, ShouldNotBeNil)
+							So(err, ShouldEqual, models.ErrDashboardWithSameNameInFolderExists)
+						})
+					})
+				})
+
+				Convey("and overwrite flag is set to true", func() {
+					shouldOverwrite := true
+
+					Convey("When updating an existing dashboard by id without current version", func() {
+						cmd := models.SaveDashboardCommand{
+							OrgId: 1,
+							Dashboard: simplejson.NewFromAny(map[string]interface{}{
+								"id":    savedDashInGeneralFolder.Id,
+								"title": "Updated title",
+							}),
+							FolderId:  savedFolder.Id,
+							Overwrite: shouldOverwrite,
+						}
+
+						res := callSaveWithResult(cmd)
+						So(res, ShouldNotBeNil)
+
+						Convey("It should update dashboard", func() {
+							query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: savedDashInGeneralFolder.Id}
+
+							err := bus.Dispatch(&query)
+							So(err, ShouldBeNil)
+							So(query.Result.Title, ShouldEqual, "Updated title")
+							So(query.Result.FolderId, ShouldEqual, savedFolder.Id)
+							So(query.Result.Version, ShouldBeGreaterThan, savedDashInGeneralFolder.Version)
+						})
+					})
+
+					Convey("When updating an existing dashboard by uid without current version", func() {
+						cmd := models.SaveDashboardCommand{
+							OrgId: 1,
+							Dashboard: simplejson.NewFromAny(map[string]interface{}{
+								"uid":   savedDashInFolder.Uid,
+								"title": "Updated title",
+							}),
+							FolderId:  0,
+							Overwrite: shouldOverwrite,
+						}
+
+						res := callSaveWithResult(cmd)
+						So(res, ShouldNotBeNil)
+
+						Convey("It should update dashboard", func() {
+							query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: savedDashInFolder.Id}
+
+							err := bus.Dispatch(&query)
+							So(err, ShouldBeNil)
+							So(query.Result.Title, ShouldEqual, "Updated title")
+							So(query.Result.FolderId, ShouldEqual, 0)
+							So(query.Result.Version, ShouldBeGreaterThan, savedDashInFolder.Version)
+						})
+					})
+
+					Convey("When updating uid for existing dashboard using id", func() {
+						cmd := models.SaveDashboardCommand{
+							OrgId: 1,
+							Dashboard: simplejson.NewFromAny(map[string]interface{}{
+								"id":    savedDashInFolder.Id,
+								"uid":   "new-uid",
+								"title": savedDashInFolder.Title,
+							}),
+							Overwrite: shouldOverwrite,
+						}
+
+						res := callSaveWithResult(cmd)
+
+						Convey("It should update dashboard", func() {
+							So(res, ShouldNotBeNil)
+							So(res.Id, ShouldEqual, savedDashInFolder.Id)
+							So(res.Uid, ShouldEqual, "new-uid")
+
+							query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: savedDashInFolder.Id}
+
+							err := bus.Dispatch(&query)
+							So(err, ShouldBeNil)
+							So(query.Result.Uid, ShouldEqual, "new-uid")
+							So(query.Result.Version, ShouldBeGreaterThan, savedDashInFolder.Version)
+						})
+					})
+
+					Convey("When updating uid to an existing uid for existing dashboard using id", func() {
+						cmd := models.SaveDashboardCommand{
+							OrgId: 1,
+							Dashboard: simplejson.NewFromAny(map[string]interface{}{
+								"id":    savedDashInFolder.Id,
+								"uid":   savedDashInGeneralFolder.Uid,
+								"title": savedDashInFolder.Title,
+							}),
+							Overwrite: shouldOverwrite,
+						}
+
+						err := callSaveWithError(cmd)
+
+						Convey("It should result in same uid exists error", func() {
+							So(err, ShouldNotBeNil)
+							So(err, ShouldEqual, models.ErrDashboardWithSameUIDExists)
+						})
+					})
+
+					Convey("When creating a dashboard with same name as dashboard in other folder", func() {
+						cmd := models.SaveDashboardCommand{
+							OrgId: testOrgId,
+							Dashboard: simplejson.NewFromAny(map[string]interface{}{
+								"id":    nil,
+								"title": savedDashInFolder.Title,
+							}),
+							FolderId:  savedDashInFolder.FolderId,
+							Overwrite: shouldOverwrite,
+						}
+
+						res := callSaveWithResult(cmd)
+
+						Convey("It should overwrite existing dashboard", func() {
+							So(res, ShouldNotBeNil)
+							So(res.Id, ShouldEqual, savedDashInFolder.Id)
+							So(res.Uid, ShouldEqual, savedDashInFolder.Uid)
+
+							query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: res.Id}
+
+							err := bus.Dispatch(&query)
+							So(err, ShouldBeNil)
+							So(query.Result.Id, ShouldEqual, res.Id)
+							So(query.Result.Uid, ShouldEqual, res.Uid)
+						})
+					})
+
+					Convey("When creating a dashboard with same name as dashboard in General folder", func() {
+						cmd := models.SaveDashboardCommand{
+							OrgId: testOrgId,
+							Dashboard: simplejson.NewFromAny(map[string]interface{}{
+								"id":    nil,
+								"title": savedDashInGeneralFolder.Title,
+							}),
+							FolderId:  savedDashInGeneralFolder.FolderId,
+							Overwrite: shouldOverwrite,
+						}
+
+						res := callSaveWithResult(cmd)
+
+						Convey("It should overwrite existing dashboard", func() {
+							So(res, ShouldNotBeNil)
+							So(res.Id, ShouldEqual, savedDashInGeneralFolder.Id)
+							So(res.Uid, ShouldEqual, savedDashInGeneralFolder.Uid)
+
+							query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: res.Id}
+
+							err := bus.Dispatch(&query)
+							So(err, ShouldBeNil)
+							So(query.Result.Id, ShouldEqual, res.Id)
+							So(query.Result.Uid, ShouldEqual, res.Uid)
+						})
+					})
+
+					Convey("When trying to update existing folder to a dashboard using id", func() {
+						cmd := models.SaveDashboardCommand{
+							OrgId: 1,
+							Dashboard: simplejson.NewFromAny(map[string]interface{}{
+								"id":    savedFolder.Id,
+								"title": "new title",
+							}),
+							IsFolder:  false,
+							Overwrite: shouldOverwrite,
+						}
+
+						err := callSaveWithError(cmd)
+
+						Convey("It should result in type mismatch error", func() {
+							So(err, ShouldNotBeNil)
+							So(err, ShouldEqual, models.ErrDashboardTypeMismatch)
+						})
+					})
+
+					Convey("When trying to update existing dashboard to a folder using id", func() {
+						cmd := models.SaveDashboardCommand{
+							OrgId: 1,
+							Dashboard: simplejson.NewFromAny(map[string]interface{}{
+								"id":    savedDashInFolder.Id,
+								"title": "new folder title",
+							}),
+							IsFolder:  true,
+							Overwrite: shouldOverwrite,
+						}
+
+						err := callSaveWithError(cmd)
+
+						Convey("It should result in type mismatch error", func() {
+							So(err, ShouldNotBeNil)
+							So(err, ShouldEqual, models.ErrDashboardTypeMismatch)
+						})
+					})
+
+					Convey("When trying to update existing folder to a dashboard using uid", func() {
+						cmd := models.SaveDashboardCommand{
+							OrgId: 1,
+							Dashboard: simplejson.NewFromAny(map[string]interface{}{
+								"uid":   savedFolder.Uid,
+								"title": "new title",
+							}),
+							IsFolder:  false,
+							Overwrite: shouldOverwrite,
+						}
+
+						err := callSaveWithError(cmd)
+
+						Convey("It should result in type mismatch error", func() {
+							So(err, ShouldNotBeNil)
+							So(err, ShouldEqual, models.ErrDashboardTypeMismatch)
+						})
+					})
+
+					Convey("When trying to update existing dashboard to a folder using uid", func() {
+						cmd := models.SaveDashboardCommand{
+							OrgId: 1,
+							Dashboard: simplejson.NewFromAny(map[string]interface{}{
+								"uid":   savedDashInFolder.Uid,
+								"title": "new folder title",
+							}),
+							IsFolder:  true,
+							Overwrite: shouldOverwrite,
+						}
+
+						err := callSaveWithError(cmd)
+
+						Convey("It should result in type mismatch error", func() {
+							So(err, ShouldNotBeNil)
+							So(err, ShouldEqual, models.ErrDashboardTypeMismatch)
+						})
+					})
+
+					Convey("When trying to update existing folder to a dashboard using title", func() {
+						cmd := models.SaveDashboardCommand{
+							OrgId: 1,
+							Dashboard: simplejson.NewFromAny(map[string]interface{}{
+								"title": savedFolder.Title,
+							}),
+							IsFolder:  false,
+							Overwrite: shouldOverwrite,
+						}
+
+						err := callSaveWithError(cmd)
+
+						Convey("It should result in dashboard with same name as folder error", func() {
+							So(err, ShouldNotBeNil)
+							So(err, ShouldEqual, models.ErrDashboardWithSameNameAsFolder)
+						})
+					})
+
+					Convey("When trying to update existing dashboard to a folder using title", func() {
+						cmd := models.SaveDashboardCommand{
+							OrgId: 1,
+							Dashboard: simplejson.NewFromAny(map[string]interface{}{
+								"title": savedDashInGeneralFolder.Title,
+							}),
+							IsFolder:  true,
+							Overwrite: shouldOverwrite,
+						}
+
+						err := callSaveWithError(cmd)
+
+						Convey("It should result in folder with same name as dashboard error", func() {
+							So(err, ShouldNotBeNil)
+							So(err, ShouldEqual, models.ErrDashboardFolderWithSameNameAsDashboard)
+						})
+					})
+				})
+			})
+		})
+	})
+}
+
+func mockDashboardGuardian(mock *mockDashboardGuarder) {
+	guardian.New = func(dashId int64, orgId int64, user *models.SignedInUser) guardian.DashboardGuardian {
+		mock.orgId = orgId
+		mock.dashId = dashId
+		mock.user = user
+		return mock
+	}
+}
+
+type mockDashboardGuarder struct {
+	dashId                      int64
+	orgId                       int64
+	user                        *models.SignedInUser
+	canSave                     bool
+	canSaveCallCounter          int
+	canEdit                     bool
+	canView                     bool
+	canAdmin                    bool
+	hasPermission               bool
+	checkPermissionBeforeRemove bool
+	checkPermissionBeforeUpdate bool
+}
+
+func (g *mockDashboardGuarder) CanSave() (bool, error) {
+	g.canSaveCallCounter++
+	return g.canSave, nil
+}
+
+func (g *mockDashboardGuarder) CanEdit() (bool, error) {
+	return g.canEdit, nil
+}
+
+func (g *mockDashboardGuarder) CanView() (bool, error) {
+	return g.canView, nil
+}
+
+func (g *mockDashboardGuarder) CanAdmin() (bool, error) {
+	return g.canAdmin, nil
+}
+
+func (g *mockDashboardGuarder) HasPermission(permission models.PermissionType) (bool, error) {
+	return g.hasPermission, nil
+}
+
+func (g *mockDashboardGuarder) CheckPermissionBeforeUpdate(permission models.PermissionType, updatePermissions []*models.DashboardAcl) (bool, error) {
+	return g.checkPermissionBeforeUpdate, nil
+}
+
+func (g *mockDashboardGuarder) GetAcl() ([]*models.DashboardAclInfoDTO, error) {
+	return nil, nil
+}
+
+type scenarioContext struct {
+	dashboardGuardianMock *mockDashboardGuarder
+}
+
+type scenarioFunc func(c *scenarioContext)
+
+func dashboardGuardianScenario(desc string, mock *mockDashboardGuarder, fn scenarioFunc) {
+	Convey(desc, func() {
+		origNewDashboardGuardian := guardian.New
+		mockDashboardGuardian(mock)
+
+		sc := &scenarioContext{
+			dashboardGuardianMock: mock,
+		}
+
+		defer func() {
+			guardian.New = origNewDashboardGuardian
+		}()
+
+		fn(sc)
+	})
+}
+
+type dashboardPermissionScenarioContext struct {
+	dashboardGuardianMock *mockDashboardGuarder
+}
+
+type dashboardPermissionScenarioFunc func(sc *dashboardPermissionScenarioContext)
+
+func dashboardPermissionScenario(desc string, mock *mockDashboardGuarder, fn dashboardPermissionScenarioFunc) {
+	Convey(desc, func() {
+		origNewDashboardGuardian := guardian.New
+		mockDashboardGuardian(mock)
+
+		sc := &dashboardPermissionScenarioContext{
+			dashboardGuardianMock: mock,
+		}
+
+		defer func() {
+			guardian.New = origNewDashboardGuardian
+		}()
+
+		fn(sc)
+	})
+}
+
+func permissionScenario(desc string, canSave bool, fn dashboardPermissionScenarioFunc) {
+	mock := &mockDashboardGuarder{
+		canSave: canSave,
+	}
+	dashboardPermissionScenario(desc, mock, fn)
+}
+
+func callSaveWithResult(cmd models.SaveDashboardCommand) *models.Dashboard {
+	dto := toSaveDashboardDto(cmd)
+	res, _ := dashboards.NewService().SaveDashboard(&dto)
+	return res
+}
+
+func callSaveWithError(cmd models.SaveDashboardCommand) error {
+	dto := toSaveDashboardDto(cmd)
+	_, err := dashboards.NewService().SaveDashboard(&dto)
+	return err
+}
+
+func dashboardServiceScenario(desc string, mock *mockDashboardGuarder, fn scenarioFunc) {
+	Convey(desc, func() {
+		origNewDashboardGuardian := guardian.New
+		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,
+		FolderId: folderId,
+		IsFolder: false,
+		Dashboard: simplejson.NewFromAny(map[string]interface{}{
+			"id":    nil,
+			"title": title,
+		}),
+	}
+
+	dto := dashboards.SaveDashboardDTO{
+		OrgId:     orgId,
+		Dashboard: cmd.GetDashboardModel(),
+		User: &models.SignedInUser{
+			UserId:  1,
+			OrgRole: models.ROLE_ADMIN,
+		},
+	}
+
+	res, err := dashboards.NewService().SaveDashboard(&dto)
+	So(err, ShouldBeNil)
+
+	return res
+}
+
+func saveTestFolder(title string, orgId int64) *models.Dashboard {
+	cmd := models.SaveDashboardCommand{
+		OrgId:    orgId,
+		FolderId: 0,
+		IsFolder: true,
+		Dashboard: simplejson.NewFromAny(map[string]interface{}{
+			"id":    nil,
+			"title": title,
+		}),
+	}
+
+	dto := dashboards.SaveDashboardDTO{
+		OrgId:     orgId,
+		Dashboard: cmd.GetDashboardModel(),
+		User: &models.SignedInUser{
+			UserId:  1,
+			OrgRole: models.ROLE_ADMIN,
+		},
+	}
+
+	res, err := dashboards.NewService().SaveDashboard(&dto)
+	So(err, ShouldBeNil)
+
+	return res
+}
+
+func toSaveDashboardDto(cmd models.SaveDashboardCommand) dashboards.SaveDashboardDTO {
+	dash := (&cmd).GetDashboardModel()
+
+	return dashboards.SaveDashboardDTO{
+		Dashboard: dash,
+		Message:   cmd.Message,
+		OrgId:     cmd.OrgId,
+		User:      &models.SignedInUser{UserId: cmd.UserId},
+		Overwrite: cmd.Overwrite,
+	}
+}

+ 40 - 341
pkg/services/sqlstore/dashboard_test.go

@@ -100,324 +100,6 @@ func TestDashboardDataAccess(t *testing.T) {
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
 			})
 			})
 
 
-			Convey("Should return not found error if no dashboard is found for update", func() {
-				cmd := m.SaveDashboardCommand{
-					OrgId:     1,
-					Overwrite: true,
-					Dashboard: simplejson.NewFromAny(map[string]interface{}{
-						"id":    float64(123412321),
-						"title": "Expect error",
-						"tags":  []interface{}{},
-					}),
-				}
-
-				err := SaveDashboard(&cmd)
-				So(err, ShouldEqual, m.ErrDashboardNotFound)
-			})
-
-			Convey("Should not be able to overwrite dashboard in another org", func() {
-				query := m.GetDashboardQuery{Slug: "test-dash-23", OrgId: 1}
-				GetDashboard(&query)
-
-				cmd := m.SaveDashboardCommand{
-					OrgId:     2,
-					Overwrite: true,
-					Dashboard: simplejson.NewFromAny(map[string]interface{}{
-						"id":    float64(query.Result.Id),
-						"title": "Expect error",
-						"tags":  []interface{}{},
-					}),
-				}
-
-				err := SaveDashboard(&cmd)
-				So(err, ShouldEqual, m.ErrDashboardNotFound)
-			})
-
-			Convey("Should be able to save dashboards with same name in different folders", func() {
-				firstSaveCmd := m.SaveDashboardCommand{
-					OrgId: 1,
-					Dashboard: simplejson.NewFromAny(map[string]interface{}{
-						"id":    nil,
-						"title": "test dash folder and title",
-						"tags":  []interface{}{},
-						"uid":   "randomHash",
-					}),
-					FolderId: 3,
-				}
-
-				err := SaveDashboard(&firstSaveCmd)
-				So(err, ShouldBeNil)
-
-				secondSaveCmd := m.SaveDashboardCommand{
-					OrgId: 1,
-					Dashboard: simplejson.NewFromAny(map[string]interface{}{
-						"id":    nil,
-						"title": "test dash folder and title",
-						"tags":  []interface{}{},
-						"uid":   "moreRandomHash",
-					}),
-					FolderId: 1,
-				}
-
-				err = SaveDashboard(&secondSaveCmd)
-				So(err, ShouldBeNil)
-				So(firstSaveCmd.Result.Id, ShouldNotEqual, secondSaveCmd.Result.Id)
-			})
-
-			Convey("Should be able to overwrite dashboard in same folder using title", func() {
-				insertTestDashboard("Dash", 1, 0, false, "prod", "webapp")
-				folder := insertTestDashboard("Folder", 1, 0, true, "prod", "webapp")
-				dashInFolder := insertTestDashboard("Dash", 1, folder.Id, false, "prod", "webapp")
-
-				cmd := m.SaveDashboardCommand{
-					OrgId: 1,
-					Dashboard: simplejson.NewFromAny(map[string]interface{}{
-						"title": "Dash",
-					}),
-					FolderId:  folder.Id,
-					Overwrite: true,
-				}
-
-				err := SaveDashboard(&cmd)
-				So(err, ShouldBeNil)
-				So(cmd.Result.Id, ShouldEqual, dashInFolder.Id)
-				So(cmd.Result.Uid, ShouldEqual, dashInFolder.Uid)
-			})
-
-			Convey("Should be able to overwrite dashboard in General folder using title", func() {
-				dashInGeneral := insertTestDashboard("Dash", 1, 0, false, "prod", "webapp")
-				folder := insertTestDashboard("Folder", 1, 0, true, "prod", "webapp")
-				insertTestDashboard("Dash", 1, folder.Id, false, "prod", "webapp")
-
-				cmd := m.SaveDashboardCommand{
-					OrgId: 1,
-					Dashboard: simplejson.NewFromAny(map[string]interface{}{
-						"title": "Dash",
-					}),
-					FolderId:  0,
-					Overwrite: true,
-				}
-
-				err := SaveDashboard(&cmd)
-				So(err, ShouldBeNil)
-				So(cmd.Result.Id, ShouldEqual, dashInGeneral.Id)
-				So(cmd.Result.Uid, ShouldEqual, dashInGeneral.Uid)
-			})
-
-			Convey("Should not be able to overwrite folder with dashboard in general folder using title", func() {
-				cmd := m.SaveDashboardCommand{
-					OrgId: 1,
-					Dashboard: simplejson.NewFromAny(map[string]interface{}{
-						"title": savedFolder.Title,
-					}),
-					FolderId:  0,
-					IsFolder:  false,
-					Overwrite: true,
-				}
-
-				err := SaveDashboard(&cmd)
-				So(err, ShouldEqual, m.ErrDashboardWithSameNameAsFolder)
-			})
-
-			Convey("Should not be able to overwrite folder with dashboard in folder using title", func() {
-				cmd := m.SaveDashboardCommand{
-					OrgId: 1,
-					Dashboard: simplejson.NewFromAny(map[string]interface{}{
-						"title": savedFolder.Title,
-					}),
-					FolderId:  savedFolder.Id,
-					IsFolder:  false,
-					Overwrite: true,
-				}
-
-				err := SaveDashboard(&cmd)
-				So(err, ShouldEqual, m.ErrDashboardWithSameNameAsFolder)
-			})
-
-			Convey("Should not be able to overwrite folder with dashboard using id", func() {
-				cmd := m.SaveDashboardCommand{
-					OrgId: 1,
-					Dashboard: simplejson.NewFromAny(map[string]interface{}{
-						"id":    savedFolder.Id,
-						"title": "new title",
-					}),
-					IsFolder:  false,
-					Overwrite: true,
-				}
-
-				err := SaveDashboard(&cmd)
-				So(err, ShouldEqual, m.ErrDashboardTypeMismatch)
-			})
-
-			Convey("Should not be able to overwrite dashboard with folder using id", func() {
-				cmd := m.SaveDashboardCommand{
-					OrgId: 1,
-					Dashboard: simplejson.NewFromAny(map[string]interface{}{
-						"id":    savedDash.Id,
-						"title": "new folder title",
-					}),
-					IsFolder:  true,
-					Overwrite: true,
-				}
-
-				err := SaveDashboard(&cmd)
-				So(err, ShouldEqual, m.ErrDashboardTypeMismatch)
-			})
-
-			Convey("Should not be able to overwrite folder with dashboard using uid", func() {
-				cmd := m.SaveDashboardCommand{
-					OrgId: 1,
-					Dashboard: simplejson.NewFromAny(map[string]interface{}{
-						"uid":   savedFolder.Uid,
-						"title": "new title",
-					}),
-					IsFolder:  false,
-					Overwrite: true,
-				}
-
-				err := SaveDashboard(&cmd)
-				So(err, ShouldEqual, m.ErrDashboardTypeMismatch)
-			})
-
-			Convey("Should not be able to overwrite dashboard with folder using uid", func() {
-				cmd := m.SaveDashboardCommand{
-					OrgId: 1,
-					Dashboard: simplejson.NewFromAny(map[string]interface{}{
-						"uid":   savedDash.Uid,
-						"title": "new folder title",
-					}),
-					IsFolder:  true,
-					Overwrite: true,
-				}
-
-				err := SaveDashboard(&cmd)
-				So(err, ShouldEqual, m.ErrDashboardTypeMismatch)
-			})
-
-			Convey("Should not be able to save dashboard with same name in the same folder without overwrite", func() {
-				firstSaveCmd := m.SaveDashboardCommand{
-					OrgId: 1,
-					Dashboard: simplejson.NewFromAny(map[string]interface{}{
-						"id":    nil,
-						"title": "test dash folder and title",
-						"tags":  []interface{}{},
-						"uid":   "randomHash",
-					}),
-					FolderId: 3,
-				}
-
-				err := SaveDashboard(&firstSaveCmd)
-				So(err, ShouldBeNil)
-
-				secondSaveCmd := m.SaveDashboardCommand{
-					OrgId: 1,
-					Dashboard: simplejson.NewFromAny(map[string]interface{}{
-						"id":    nil,
-						"title": "test dash folder and title",
-						"tags":  []interface{}{},
-						"uid":   "moreRandomHash",
-					}),
-					FolderId: 3,
-				}
-
-				err = SaveDashboard(&secondSaveCmd)
-				So(err, ShouldEqual, m.ErrDashboardWithSameNameInFolderExists)
-			})
-
-			Convey("Should be able to save and update dashboard using same uid", func() {
-				cmd := m.SaveDashboardCommand{
-					OrgId: 1,
-					Dashboard: simplejson.NewFromAny(map[string]interface{}{
-						"id":    nil,
-						"uid":   "dsfalkjngailuedt",
-						"title": "test dash 23",
-					}),
-				}
-
-				err := SaveDashboard(&cmd)
-				So(err, ShouldBeNil)
-				err = SaveDashboard(&cmd)
-				So(err, ShouldBeNil)
-			})
-
-			Convey("Should be able to update dashboard using uid", func() {
-				cmd := m.SaveDashboardCommand{
-					OrgId: 1,
-					Dashboard: simplejson.NewFromAny(map[string]interface{}{
-						"uid":   savedDash.Uid,
-						"title": "new title",
-					}),
-					FolderId:  0,
-					Overwrite: true,
-				}
-
-				err := SaveDashboard(&cmd)
-				So(err, ShouldBeNil)
-
-				Convey("Should be able to get updated dashboard by uid", func() {
-					query := m.GetDashboardQuery{
-						Uid:   savedDash.Uid,
-						OrgId: 1,
-					}
-
-					err := GetDashboard(&query)
-					So(err, ShouldBeNil)
-
-					So(query.Result.Id, ShouldEqual, savedDash.Id)
-					So(query.Result.Title, ShouldEqual, "new title")
-					So(query.Result.FolderId, ShouldEqual, 0)
-				})
-			})
-
-			Convey("Should be able to update dashboard with the same title and folder id", func() {
-				cmd := m.SaveDashboardCommand{
-					OrgId: 1,
-					Dashboard: simplejson.NewFromAny(map[string]interface{}{
-						"uid":   "randomHash",
-						"title": "folderId",
-						"style": "light",
-						"tags":  []interface{}{},
-					}),
-					FolderId: 2,
-				}
-
-				err := SaveDashboard(&cmd)
-				So(err, ShouldBeNil)
-				So(cmd.Result.FolderId, ShouldEqual, 2)
-
-				cmd = m.SaveDashboardCommand{
-					OrgId: 1,
-					Dashboard: simplejson.NewFromAny(map[string]interface{}{
-						"id":      cmd.Result.Id,
-						"uid":     "randomHash",
-						"title":   "folderId",
-						"style":   "dark",
-						"version": cmd.Result.Version,
-						"tags":    []interface{}{},
-					}),
-					FolderId: 2,
-				}
-
-				err = SaveDashboard(&cmd)
-				So(err, ShouldBeNil)
-			})
-
-			Convey("Should be able to update using uid without id and overwrite", func() {
-				cmd := m.SaveDashboardCommand{
-					OrgId: 1,
-					Dashboard: simplejson.NewFromAny(map[string]interface{}{
-						"uid":     savedDash.Uid,
-						"title":   "folderId",
-						"version": savedDash.Version,
-						"tags":    []interface{}{},
-					}),
-					FolderId: savedDash.FolderId,
-				}
-
-				err := SaveDashboard(&cmd)
-				So(err, ShouldBeNil)
-			})
-
 			Convey("Should retry generation of uid once if it fails.", func() {
 			Convey("Should retry generation of uid once if it fails.", func() {
 				timesCalled := 0
 				timesCalled := 0
 				generateNewUid = func() string {
 				generateNewUid = func() string {
@@ -499,6 +181,36 @@ func TestDashboardDataAccess(t *testing.T) {
 				So(len(query.Result), ShouldEqual, 0)
 				So(len(query.Result), ShouldEqual, 0)
 			})
 			})
 
 
+			Convey("Should return error if no dashboard is found for update when dashboard id is greater than zero", func() {
+				cmd := m.SaveDashboardCommand{
+					OrgId:     1,
+					Overwrite: true,
+					Dashboard: simplejson.NewFromAny(map[string]interface{}{
+						"id":    float64(123412321),
+						"title": "Expect error",
+						"tags":  []interface{}{},
+					}),
+				}
+
+				err := SaveDashboard(&cmd)
+				So(err, ShouldEqual, m.ErrDashboardNotFound)
+			})
+
+			Convey("Should not return error if no dashboard is found for update when dashboard id is zero", func() {
+				cmd := m.SaveDashboardCommand{
+					OrgId:     1,
+					Overwrite: true,
+					Dashboard: simplejson.NewFromAny(map[string]interface{}{
+						"id":    0,
+						"title": "New dash",
+						"tags":  []interface{}{},
+					}),
+				}
+
+				err := SaveDashboard(&cmd)
+				So(err, ShouldBeNil)
+			})
+
 			Convey("Should be able to get dashboard tags", func() {
 			Convey("Should be able to get dashboard tags", func() {
 				query := m.GetDashboardTagsQuery{OrgId: 1}
 				query := m.GetDashboardTagsQuery{OrgId: 1}
 
 
@@ -512,7 +224,7 @@ func TestDashboardDataAccess(t *testing.T) {
 				query := search.FindPersistedDashboardsQuery{
 				query := search.FindPersistedDashboardsQuery{
 					Title:        "1 test dash folder",
 					Title:        "1 test dash folder",
 					OrgId:        1,
 					OrgId:        1,
-					SignedInUser: &m.SignedInUser{OrgId: 1},
+					SignedInUser: &m.SignedInUser{OrgId: 1, OrgRole: m.ROLE_EDITOR},
 				}
 				}
 
 
 				err := SearchDashboards(&query)
 				err := SearchDashboards(&query)
@@ -529,7 +241,7 @@ func TestDashboardDataAccess(t *testing.T) {
 				query := search.FindPersistedDashboardsQuery{
 				query := search.FindPersistedDashboardsQuery{
 					OrgId:        1,
 					OrgId:        1,
 					FolderIds:    []int64{savedFolder.Id},
 					FolderIds:    []int64{savedFolder.Id},
-					SignedInUser: &m.SignedInUser{OrgId: 1},
+					SignedInUser: &m.SignedInUser{OrgId: 1, OrgRole: m.ROLE_EDITOR},
 				}
 				}
 
 
 				err := SearchDashboards(&query)
 				err := SearchDashboards(&query)
@@ -549,7 +261,7 @@ func TestDashboardDataAccess(t *testing.T) {
 				Convey("should be able to find two dashboards by id", func() {
 				Convey("should be able to find two dashboards by id", func() {
 					query := search.FindPersistedDashboardsQuery{
 					query := search.FindPersistedDashboardsQuery{
 						DashboardIds: []int64{2, 3},
 						DashboardIds: []int64{2, 3},
-						SignedInUser: &m.SignedInUser{OrgId: 1},
+						SignedInUser: &m.SignedInUser{OrgId: 1, OrgRole: m.ROLE_EDITOR},
 					}
 					}
 
 
 					err := SearchDashboards(&query)
 					err := SearchDashboards(&query)
@@ -578,7 +290,10 @@ func TestDashboardDataAccess(t *testing.T) {
 				})
 				})
 
 
 				Convey("Should be able to search for starred dashboards", func() {
 				Convey("Should be able to search for starred dashboards", func() {
-					query := search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: 10, OrgId: 1}, IsStarred: true}
+					query := search.FindPersistedDashboardsQuery{
+						SignedInUser: &m.SignedInUser{UserId: 10, OrgId: 1, OrgRole: m.ROLE_EDITOR},
+						IsStarred:    true,
+					}
 					err := SearchDashboards(&query)
 					err := SearchDashboards(&query)
 
 
 					So(err, ShouldBeNil)
 					So(err, ShouldBeNil)
@@ -624,6 +339,9 @@ func insertTestDashboard(title string, orgId int64, folderId int64, isFolder boo
 	err := SaveDashboard(&cmd)
 	err := SaveDashboard(&cmd)
 	So(err, ShouldBeNil)
 	So(err, ShouldBeNil)
 
 
+	cmd.Result.Data.Set("id", cmd.Result.Id)
+	cmd.Result.Data.Set("uid", cmd.Result.Uid)
+
 	return cmd.Result
 	return cmd.Result
 }
 }
 
 
@@ -660,25 +378,6 @@ func createUser(name string, role string, isAdmin bool) m.User {
 	return currentUserCmd.Result
 	return currentUserCmd.Result
 }
 }
 
 
-func updateTestDashboardWithAcl(dashId int64, userId int64, permissions m.PermissionType) int64 {
-	cmd := &m.SetDashboardAclCommand{
-		OrgId:       1,
-		UserId:      userId,
-		DashboardId: dashId,
-		Permission:  permissions,
-	}
-
-	err := SetDashboardAcl(cmd)
-	So(err, ShouldBeNil)
-
-	return cmd.Result.Id
-}
-
-func removeAcl(aclId int64) {
-	err := RemoveDashboardAcl(&m.RemoveDashboardAclCommand{AclId: aclId, OrgId: 1})
-	So(err, ShouldBeNil)
-}
-
 func moveDashboard(orgId int64, dashboard *simplejson.Json, newFolderId int64) *m.Dashboard {
 func moveDashboard(orgId int64, dashboard *simplejson.Json, newFolderId int64) *m.Dashboard {
 	cmd := m.SaveDashboardCommand{
 	cmd := m.SaveDashboardCommand{
 		OrgId:     orgId,
 		OrgId:     orgId,

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

@@ -12,7 +12,7 @@ import (
 )
 )
 
 
 func updateTestDashboard(dashboard *m.Dashboard, data map[string]interface{}) {
 func updateTestDashboard(dashboard *m.Dashboard, data map[string]interface{}) {
-	data["uid"] = dashboard.Uid
+	data["id"] = dashboard.Id
 
 
 	saveCmd := m.SaveDashboardCommand{
 	saveCmd := m.SaveDashboardCommand{
 		OrgId:     dashboard.OrgId,
 		OrgId:     dashboard.OrgId,

+ 3 - 0
pkg/services/sqlstore/datasource.go

@@ -27,6 +27,9 @@ func GetDataSourceById(query *m.GetDataSourceByIdQuery) error {
 
 
 	datasource := m.DataSource{OrgId: query.OrgId, Id: query.Id}
 	datasource := m.DataSource{OrgId: query.OrgId, Id: query.Id}
 	has, err := x.Get(&datasource)
 	has, err := x.Get(&datasource)
+	if err != nil {
+		return err
+	}
 
 
 	if !has {
 	if !has {
 		return m.ErrDataSourceNotFound
 		return m.ErrDataSourceNotFound

+ 0 - 48
pkg/services/sqlstore/datasource_test.go

@@ -1,61 +1,13 @@
 package sqlstore
 package sqlstore
 
 
 import (
 import (
-	"os"
-	"strings"
 	"testing"
 	"testing"
 
 
-	"github.com/go-xorm/xorm"
-
 	. "github.com/smartystreets/goconvey/convey"
 	. "github.com/smartystreets/goconvey/convey"
 
 
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
-)
-
-var (
-	dbSqlite   = "sqlite"
-	dbMySql    = "mysql"
-	dbPostgres = "postgres"
 )
 )
 
 
-func InitTestDB(t *testing.T) *xorm.Engine {
-	selectedDb := dbSqlite
-	//selectedDb := dbMySql
-	//selectedDb := dbPostgres
-
-	var x *xorm.Engine
-	var err error
-
-	// environment variable present for test db?
-	if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present {
-		selectedDb = db
-	}
-
-	switch strings.ToLower(selectedDb) {
-	case dbMySql:
-		x, err = xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr)
-	case dbPostgres:
-		x, err = xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, sqlutil.TestDB_Postgres.ConnStr)
-	default:
-		x, err = xorm.NewEngine(sqlutil.TestDB_Sqlite3.DriverName, sqlutil.TestDB_Sqlite3.ConnStr)
-	}
-
-	// x.ShowSQL()
-
-	if err != nil {
-		t.Fatalf("Failed to init in memory sqllite3 db %v", err)
-	}
-
-	sqlutil.CleanDB(x)
-
-	if err := SetEngine(x); err != nil {
-		t.Fatal(err)
-	}
-
-	return x
-}
-
 type Test struct {
 type Test struct {
 	Id   int64
 	Id   int64
 	Name string
 	Name string

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

@@ -21,7 +21,7 @@ func CreateLoginAttempt(cmd *m.CreateLoginAttemptCommand) error {
 		loginAttempt := m.LoginAttempt{
 		loginAttempt := m.LoginAttempt{
 			Username:  cmd.Username,
 			Username:  cmd.Username,
 			IpAddress: cmd.IpAddress,
 			IpAddress: cmd.IpAddress,
-			Created:   getTimeNow(),
+			Created:   getTimeNow().Unix(),
 		}
 		}
 
 
 		if _, err := sess.Insert(&loginAttempt); err != nil {
 		if _, err := sess.Insert(&loginAttempt); err != nil {
@@ -37,8 +37,8 @@ func CreateLoginAttempt(cmd *m.CreateLoginAttemptCommand) error {
 func DeleteOldLoginAttempts(cmd *m.DeleteOldLoginAttemptsCommand) error {
 func DeleteOldLoginAttempts(cmd *m.DeleteOldLoginAttemptsCommand) error {
 	return inTransaction(func(sess *DBSession) error {
 	return inTransaction(func(sess *DBSession) error {
 		var maxId int64
 		var maxId int64
-		sql := "SELECT max(id) as id FROM login_attempt WHERE created < " + dialect.DateTimeFunc("?")
-		result, err := sess.Query(sql, cmd.OlderThan)
+		sql := "SELECT max(id) as id FROM login_attempt WHERE created < ?"
+		result, err := sess.Query(sql, cmd.OlderThan.Unix())
 
 
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -66,7 +66,7 @@ func GetUserLoginAttemptCount(query *m.GetUserLoginAttemptCountQuery) error {
 	loginAttempt := new(m.LoginAttempt)
 	loginAttempt := new(m.LoginAttempt)
 	total, err := x.
 	total, err := x.
 		Where("username = ?", query.Username).
 		Where("username = ?", query.Username).
-		And("created >="+dialect.DateTimeFunc("?"), query.Since).
+		And("created >= ?", query.Since.Unix()).
 		Count(loginAttempt)
 		Count(loginAttempt)
 
 
 	if err != nil {
 	if err != nil {

+ 21 - 0
pkg/services/sqlstore/migrations/common.go

@@ -24,3 +24,24 @@ func addTableRenameMigration(mg *Migrator, oldName string, newName string, versi
 	migrationId := fmt.Sprintf("Rename table %s to %s - %s", oldName, newName, versionSuffix)
 	migrationId := fmt.Sprintf("Rename table %s to %s - %s", oldName, newName, versionSuffix)
 	mg.AddMigration(migrationId, NewRenameTableMigration(oldName, newName))
 	mg.AddMigration(migrationId, NewRenameTableMigration(oldName, newName))
 }
 }
+
+func addTableReplaceMigrations(mg *Migrator, from Table, to Table, migrationVersion int64, tableDataMigration map[string]string) {
+	fromV := version(migrationVersion - 1)
+	toV := version(migrationVersion)
+	tmpTableName := to.Name + "_tmp_qwerty"
+
+	createTable := fmt.Sprintf("create %v %v", to.Name, toV)
+	copyTableData := fmt.Sprintf("copy %v %v to %v", to.Name, fromV, toV)
+	dropTable := fmt.Sprintf("drop %v", tmpTableName)
+
+	addDropAllIndicesMigrations(mg, fromV, from)
+	addTableRenameMigration(mg, from.Name, tmpTableName, fromV)
+	mg.AddMigration(createTable, NewAddTableMigration(to))
+	addTableIndicesMigrations(mg, toV, to)
+	mg.AddMigration(copyTableData, NewCopyTableDataMigration(to.Name, tmpTableName, tableDataMigration))
+	mg.AddMigration(dropTable, NewDropTableMigration(tmpTableName))
+}
+
+func version(v int64) string {
+	return fmt.Sprintf("v%v", v)
+}

+ 38 - 1
pkg/services/sqlstore/migrations/dashboard_mig.go

@@ -1,6 +1,8 @@
 package migrations
 package migrations
 
 
-import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
+import (
+	. "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
+)
 
 
 func addDashboardMigration(mg *Migrator) {
 func addDashboardMigration(mg *Migrator) {
 	var dashboardV1 = Table{
 	var dashboardV1 = Table{
@@ -176,4 +178,39 @@ func addDashboardMigration(mg *Migrator) {
 		Cols: []string{"org_id", "folder_id", "title"}, Type: UniqueIndex,
 		Cols: []string{"org_id", "folder_id", "title"}, Type: UniqueIndex,
 	}))
 	}))
 
 
+	dashboardExtrasTable := Table{
+		Name: "dashboard_provisioning",
+		Columns: []*Column{
+			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
+			{Name: "dashboard_id", Type: DB_BigInt, Nullable: true},
+			{Name: "name", Type: DB_NVarchar, Length: 150, Nullable: false},
+			{Name: "external_id", Type: DB_Text, Nullable: false},
+			{Name: "updated", Type: DB_DateTime, Nullable: false},
+		},
+		Indices: []*Index{},
+	}
+
+	mg.AddMigration("create dashboard_provisioning", NewAddTableMigration(dashboardExtrasTable))
+
+	dashboardExtrasTableV2 := Table{
+		Name: "dashboard_provisioning",
+		Columns: []*Column{
+			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
+			{Name: "dashboard_id", Type: DB_BigInt, Nullable: true},
+			{Name: "name", Type: DB_NVarchar, Length: 150, Nullable: false},
+			{Name: "external_id", Type: DB_Text, Nullable: false},
+			{Name: "updated", Type: DB_Int, Default: "0", Nullable: false},
+		},
+		Indices: []*Index{
+			{Cols: []string{"dashboard_id"}},
+			{Cols: []string{"dashboard_id", "name"}, Type: IndexType},
+		},
+	}
+
+	addTableReplaceMigrations(mg, dashboardExtrasTable, dashboardExtrasTableV2, 2, map[string]string{
+		"id":           "id",
+		"dashboard_id": "dashboard_id",
+		"name":         "name",
+		"external_id":  "external_id",
+	})
 }
 }

+ 19 - 0
pkg/services/sqlstore/migrations/login_attempt_mig.go

@@ -20,4 +20,23 @@ func addLoginAttemptMigrations(mg *Migrator) {
 	mg.AddMigration("create login attempt table", NewAddTableMigration(loginAttemptV1))
 	mg.AddMigration("create login attempt table", NewAddTableMigration(loginAttemptV1))
 	// add indices
 	// add indices
 	mg.AddMigration("add index login_attempt.username", NewAddIndexMigration(loginAttemptV1, loginAttemptV1.Indices[0]))
 	mg.AddMigration("add index login_attempt.username", NewAddIndexMigration(loginAttemptV1, loginAttemptV1.Indices[0]))
+
+	loginAttemptV2 := Table{
+		Name: "login_attempt",
+		Columns: []*Column{
+			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
+			{Name: "username", Type: DB_NVarchar, Length: 190, Nullable: false},
+			{Name: "ip_address", Type: DB_NVarchar, Length: 30, Nullable: false},
+			{Name: "created", Type: DB_Int, Default: "0", Nullable: false},
+		},
+		Indices: []*Index{
+			{Cols: []string{"username"}},
+		},
+	}
+
+	addTableReplaceMigrations(mg, loginAttemptV1, loginAttemptV2, 2, map[string]string{
+		"id":         "id",
+		"username":   "username",
+		"ip_address": "ip_address",
+	})
 }
 }

+ 23 - 20
pkg/services/sqlstore/migrations/migrations_test.go

@@ -14,13 +14,15 @@ import (
 var indexTypes = []string{"Unknown", "INDEX", "UNIQUE INDEX"}
 var indexTypes = []string{"Unknown", "INDEX", "UNIQUE INDEX"}
 
 
 func TestMigrations(t *testing.T) {
 func TestMigrations(t *testing.T) {
-	//log.NewLogger(0, "console", `{"level": 0}`)
-
 	testDBs := []sqlutil.TestDB{
 	testDBs := []sqlutil.TestDB{
 		sqlutil.TestDB_Sqlite3,
 		sqlutil.TestDB_Sqlite3,
 	}
 	}
 
 
 	for _, testDB := range testDBs {
 	for _, testDB := range testDBs {
+		sql := `select count(*) as count from migration_log`
+		r := struct {
+			Count int64
+		}{}
 
 
 		Convey("Initial "+testDB.DriverName+" migration", t, func() {
 		Convey("Initial "+testDB.DriverName+" migration", t, func() {
 			x, err := xorm.NewEngine(testDB.DriverName, testDB.ConnStr)
 			x, err := xorm.NewEngine(testDB.DriverName, testDB.ConnStr)
@@ -28,30 +30,31 @@ func TestMigrations(t *testing.T) {
 
 
 			sqlutil.CleanDB(x)
 			sqlutil.CleanDB(x)
 
 
+			has, err := x.SQL(sql).Get(&r)
+			So(err, ShouldNotBeNil)
+
 			mg := NewMigrator(x)
 			mg := NewMigrator(x)
 			AddMigrations(mg)
 			AddMigrations(mg)
 
 
 			err = mg.Start()
 			err = mg.Start()
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 
 
-			// tables, err := x.DBMetas()
-			// So(err, ShouldBeNil)
-			//
-			// fmt.Printf("\nDB Schema after migration: table count: %v\n", len(tables))
-			//
-			// for _, table := range tables {
-			// 	fmt.Printf("\nTable: %v \n", table.Name)
-			// 	for _, column := range table.Columns() {
-			// 		fmt.Printf("\t %v \n", column.String(x.Dialect()))
-			// 	}
-			//
-			// 	if len(table.Indexes) > 0 {
-			// 		fmt.Printf("\n\tIndexes:\n")
-			// 		for _, index := range table.Indexes {
-			// 			fmt.Printf("\t %v (%v) %v \n", index.Name, strings.Join(index.Cols, ","), indexTypes[index.Type])
-			// 		}
-			// 	}
-			// }
+			has, err = x.SQL(sql).Get(&r)
+			So(err, ShouldBeNil)
+			So(has, ShouldBeTrue)
+			expectedMigrations := mg.MigrationsCount() - 2 //we currently skip to migrations. We should rewrite skipped migrations to write in the log as well. until then we have to keep this
+			So(r.Count, ShouldEqual, expectedMigrations)
+
+			mg = NewMigrator(x)
+			AddMigrations(mg)
+
+			err = mg.Start()
+			So(err, ShouldBeNil)
+
+			has, err = x.SQL(sql).Get(&r)
+			So(err, ShouldBeNil)
+			So(has, ShouldBeTrue)
+			So(r.Count, ShouldEqual, expectedMigrations)
 		})
 		})
 	}
 	}
 }
 }

+ 4 - 0
pkg/services/sqlstore/migrator/migrator.go

@@ -35,6 +35,10 @@ func NewMigrator(engine *xorm.Engine) *Migrator {
 	return mg
 	return mg
 }
 }
 
 
+func (mg *Migrator) MigrationsCount() int {
+	return len(mg.migrations)
+}
+
 func (mg *Migrator) AddMigration(id string, m Migration) {
 func (mg *Migrator) AddMigration(id string, m Migration) {
 	m.SetId(id)
 	m.SetId(id)
 	mg.migrations = append(mg.migrations, m)
 	mg.migrations = append(mg.migrations, m)

+ 13 - 2
pkg/services/sqlstore/org_test.go

@@ -199,10 +199,13 @@ func TestAccountDataAccess(t *testing.T) {
 					So(err, ShouldBeNil)
 					So(err, ShouldBeNil)
 					So(len(query.Result), ShouldEqual, 3)
 					So(len(query.Result), ShouldEqual, 3)
 
 
-					err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: ac1.OrgId, UserId: ac3.Id, Permission: m.PERMISSION_EDIT})
+					dash1 := insertTestDashboard("1 test dash", ac1.OrgId, 0, false, "prod", "webapp")
+					dash2 := insertTestDashboard("2 test dash", ac3.OrgId, 0, false, "prod", "webapp")
+
+					err = testHelperUpdateDashboardAcl(dash1.Id, m.DashboardAcl{DashboardId: dash1.Id, OrgId: ac1.OrgId, UserId: ac3.Id, Permission: m.PERMISSION_EDIT})
 					So(err, ShouldBeNil)
 					So(err, ShouldBeNil)
 
 
-					err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 2, OrgId: ac3.OrgId, UserId: ac3.Id, Permission: m.PERMISSION_EDIT})
+					err = testHelperUpdateDashboardAcl(dash2.Id, m.DashboardAcl{DashboardId: dash2.Id, OrgId: ac3.OrgId, UserId: ac3.Id, Permission: m.PERMISSION_EDIT})
 					So(err, ShouldBeNil)
 					So(err, ShouldBeNil)
 
 
 					Convey("When org user is deleted", func() {
 					Convey("When org user is deleted", func() {
@@ -234,3 +237,11 @@ func TestAccountDataAccess(t *testing.T) {
 		})
 		})
 	})
 	})
 }
 }
+
+func testHelperUpdateDashboardAcl(dashboardId int64, items ...m.DashboardAcl) error {
+	cmd := m.UpdateDashboardAclCommand{DashboardId: dashboardId}
+	for _, item := range items {
+		cmd.Items = append(cmd.Items, &item)
+	}
+	return UpdateDashboardAcl(&cmd)
+}

+ 0 - 4
pkg/services/sqlstore/playlist.go

@@ -1,8 +1,6 @@
 package sqlstore
 package sqlstore
 
 
 import (
 import (
-	"fmt"
-
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
 )
 )
@@ -25,8 +23,6 @@ func CreatePlaylist(cmd *m.CreatePlaylistCommand) error {
 
 
 	_, err := x.Insert(&playlist)
 	_, err := x.Insert(&playlist)
 
 
-	fmt.Printf("%v", playlist.Id)
-
 	playlistItems := make([]m.PlaylistItem, 0)
 	playlistItems := make([]m.PlaylistItem, 0)
 	for _, item := range cmd.Items {
 	for _, item := range cmd.Items {
 		playlistItems = append(playlistItems, m.PlaylistItem{
 		playlistItems = append(playlistItems, m.PlaylistItem{

+ 6 - 25
pkg/services/sqlstore/search_builder.go

@@ -1,7 +1,6 @@
 package sqlstore
 package sqlstore
 
 
 import (
 import (
-	"bytes"
 	"strings"
 	"strings"
 
 
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
@@ -9,6 +8,7 @@ import (
 
 
 // SearchBuilder is a builder/object mother that builds a dashboard search query
 // SearchBuilder is a builder/object mother that builds a dashboard search query
 type SearchBuilder struct {
 type SearchBuilder struct {
+	SqlBuilder
 	tags                []string
 	tags                []string
 	isStarred           bool
 	isStarred           bool
 	limit               int
 	limit               int
@@ -18,14 +18,14 @@ type SearchBuilder struct {
 	whereTypeFolder     bool
 	whereTypeFolder     bool
 	whereTypeDash       bool
 	whereTypeDash       bool
 	whereFolderIds      []int64
 	whereFolderIds      []int64
-	sql                 bytes.Buffer
-	params              []interface{}
+	permission          m.PermissionType
 }
 }
 
 
-func NewSearchBuilder(signedInUser *m.SignedInUser, limit int) *SearchBuilder {
+func NewSearchBuilder(signedInUser *m.SignedInUser, limit int, permission m.PermissionType) *SearchBuilder {
 	searchBuilder := &SearchBuilder{
 	searchBuilder := &SearchBuilder{
 		signedInUser: signedInUser,
 		signedInUser: signedInUser,
 		limit:        limit,
 		limit:        limit,
+		permission:   permission,
 	}
 	}
 
 
 	return searchBuilder
 	return searchBuilder
@@ -153,10 +153,7 @@ func (sb *SearchBuilder) buildMainQuery() {
 	sb.sql.WriteString(` WHERE `)
 	sb.sql.WriteString(` WHERE `)
 	sb.buildSearchWhereClause()
 	sb.buildSearchWhereClause()
 
 
-	sb.sql.WriteString(`
-		LIMIT ?) as ids
-	INNER JOIN dashboard on ids.id = dashboard.id
-	`)
+	sb.sql.WriteString(` LIMIT ?) as ids INNER JOIN dashboard on ids.id = dashboard.id `)
 	sb.params = append(sb.params, sb.limit)
 	sb.params = append(sb.params, sb.limit)
 }
 }
 
 
@@ -176,23 +173,7 @@ func (sb *SearchBuilder) buildSearchWhereClause() {
 		}
 		}
 	}
 	}
 
 
-	if sb.signedInUser.OrgRole != m.ROLE_ADMIN {
-		allowedDashboardsSubQuery := ` AND (dashboard.has_acl = ` + dialect.BooleanStr(false) + ` OR dashboard.id in (
-			SELECT distinct d.id AS DashboardId
-			FROM dashboard AS d
-	      		LEFT JOIN dashboard_acl as da on d.folder_id = da.dashboard_id or d.id = da.dashboard_id
-	      		LEFT JOIN team_member as ugm on ugm.team_id =  da.team_id
-	      		LEFT JOIN org_user ou on ou.role = da.role
-			WHERE
-			  d.has_acl = ` + dialect.BooleanStr(true) + ` and
-				(da.user_id = ? or ugm.user_id = ? or ou.id is not null)
-			  and d.org_id = ?
-			)
-		)`
-
-		sb.sql.WriteString(allowedDashboardsSubQuery)
-		sb.params = append(sb.params, sb.signedInUser.UserId, sb.signedInUser.UserId, sb.signedInUser.OrgId)
-	}
+	sb.writeDashboardPermissionFilter(sb.signedInUser, sb.permission)
 
 
 	if len(sb.whereTitle) > 0 {
 	if len(sb.whereTitle) > 0 {
 		sb.sql.WriteString(" AND dashboard.title " + dialect.LikeStr() + " ?")
 		sb.sql.WriteString(" AND dashboard.title " + dialect.LikeStr() + " ?")

+ 2 - 1
pkg/services/sqlstore/search_builder_test.go

@@ -16,7 +16,8 @@ func TestSearchBuilder(t *testing.T) {
 			OrgId:  1,
 			OrgId:  1,
 			UserId: 1,
 			UserId: 1,
 		}
 		}
-		sb := NewSearchBuilder(signedInUser, 1000)
+
+		sb := NewSearchBuilder(signedInUser, 1000, m.PERMISSION_VIEW)
 
 
 		Convey("When building a normal search", func() {
 		Convey("When building a normal search", func() {
 			sql, params := sb.IsStarred().WithTitle("test").ToSql()
 			sql, params := sb.IsStarred().WithTitle("test").ToSql()

+ 75 - 0
pkg/services/sqlstore/sqlbuilder.go

@@ -0,0 +1,75 @@
+package sqlstore
+
+import (
+	"bytes"
+	"strings"
+
+	m "github.com/grafana/grafana/pkg/models"
+)
+
+type SqlBuilder struct {
+	sql    bytes.Buffer
+	params []interface{}
+}
+
+func (sb *SqlBuilder) Write(sql string, params ...interface{}) {
+	sb.sql.WriteString(sql)
+
+	if len(params) > 0 {
+		sb.params = append(sb.params, params...)
+	}
+}
+
+func (sb *SqlBuilder) GetSqlString() string {
+	return sb.sql.String()
+}
+
+func (sb *SqlBuilder) AddParams(params ...interface{}) {
+	sb.params = append(sb.params, params...)
+}
+
+func (sb *SqlBuilder) writeDashboardPermissionFilter(user *m.SignedInUser, permission m.PermissionType) {
+
+	if user.OrgRole == m.ROLE_ADMIN {
+		return
+	}
+
+	okRoles := []interface{}{user.OrgRole}
+
+	if user.OrgRole == m.ROLE_EDITOR {
+		okRoles = append(okRoles, m.ROLE_VIEWER)
+	}
+
+	falseStr := dialect.BooleanStr(false)
+
+	sb.sql.WriteString(` AND
+	(
+		dashboard.id IN (
+			SELECT distinct d.id AS DashboardId
+			FROM dashboard AS d
+			 	LEFT JOIN dashboard folder on folder.id = d.folder_id
+			    LEFT JOIN dashboard_acl AS da ON
+	 			da.dashboard_id = d.id OR
+	 			da.dashboard_id = d.folder_id OR
+	 			(
+	 				-- include default permissions -->
+					da.org_id = -1 AND (
+					  (folder.id IS NOT NULL AND folder.has_acl = ` + falseStr + `) OR
+					  (folder.id IS NULL AND d.has_acl = ` + falseStr + `)
+					)
+	 			)
+				LEFT JOIN team_member as ugm on ugm.team_id = da.team_id
+			WHERE
+				d.org_id = ? AND
+				da.permission >= ? AND
+				(
+					da.user_id = ? OR
+					ugm.user_id = ? OR
+					da.role IN (?` + strings.Repeat(",?", len(okRoles)-1) + `)
+				)
+		)
+	)`)
+
+	sb.params = append(sb.params, user.OrgId, permission, user.UserId, user.UserId)
+	sb.params = append(sb.params, okRoles...)
+}

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

@@ -7,14 +7,15 @@ import (
 	"path"
 	"path"
 	"path/filepath"
 	"path/filepath"
 	"strings"
 	"strings"
+	"testing"
 
 
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/services/annotations"
 	"github.com/grafana/grafana/pkg/services/annotations"
-	"github.com/grafana/grafana/pkg/services/dashboards"
 	"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
 	"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
 	"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
 	"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
+	"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
 
 
 	"github.com/go-sql-driver/mysql"
 	"github.com/go-sql-driver/mysql"
@@ -101,7 +102,6 @@ func SetEngine(engine *xorm.Engine) (err error) {
 
 
 	// Init repo instances
 	// Init repo instances
 	annotations.SetRepository(&SqlAnnotationRepo{})
 	annotations.SetRepository(&SqlAnnotationRepo{})
-	dashboards.SetRepository(&dashboards.DashboardRepository{})
 	return nil
 	return nil
 }
 }
 
 
@@ -216,3 +216,46 @@ func LoadConfig() {
 	DbCfg.ServerCertName = sec.Key("server_cert_name").String()
 	DbCfg.ServerCertName = sec.Key("server_cert_name").String()
 	DbCfg.Path = sec.Key("path").MustString("data/grafana.db")
 	DbCfg.Path = sec.Key("path").MustString("data/grafana.db")
 }
 }
+
+var (
+	dbSqlite   = "sqlite"
+	dbMySql    = "mysql"
+	dbPostgres = "postgres"
+)
+
+func InitTestDB(t *testing.T) *xorm.Engine {
+	selectedDb := dbSqlite
+	//selectedDb := dbMySql
+	//selectedDb := dbPostgres
+
+	var x *xorm.Engine
+	var err error
+
+	// environment variable present for test db?
+	if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present {
+		selectedDb = db
+	}
+
+	switch strings.ToLower(selectedDb) {
+	case dbMySql:
+		x, err = xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr)
+	case dbPostgres:
+		x, err = xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, sqlutil.TestDB_Postgres.ConnStr)
+	default:
+		x, err = xorm.NewEngine(sqlutil.TestDB_Sqlite3.DriverName, sqlutil.TestDB_Sqlite3.ConnStr)
+	}
+
+	// x.ShowSQL()
+
+	if err != nil {
+		t.Fatalf("Failed to init in memory sqllite3 db %v", err)
+	}
+
+	sqlutil.CleanDB(x)
+
+	if err := SetEngine(x); err != nil {
+		t.Fatal(err)
+	}
+
+	return x
+}

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

@@ -11,8 +11,8 @@ type TestDB struct {
 	ConnStr    string
 	ConnStr    string
 }
 }
 
 
-var TestDB_Sqlite3 = TestDB{DriverName: "sqlite3", ConnStr: ":memory:?_loc=Local"}
-var TestDB_Mysql = TestDB{DriverName: "mysql", ConnStr: "grafana:password@tcp(localhost:3306)/grafana_tests?collation=utf8mb4_unicode_ci&loc=Local"}
+var TestDB_Sqlite3 = TestDB{DriverName: "sqlite3", ConnStr: ":memory:"}
+var TestDB_Mysql = TestDB{DriverName: "mysql", ConnStr: "grafana:password@tcp(localhost:3306)/grafana_tests?collation=utf8mb4_unicode_ci"}
 var TestDB_Postgres = TestDB{DriverName: "postgres", ConnStr: "user=grafanatest password=grafanatest host=localhost port=5432 dbname=grafanatest sslmode=disable"}
 var TestDB_Postgres = TestDB{DriverName: "postgres", ConnStr: "user=grafanatest password=grafanatest host=localhost port=5432 dbname=grafanatest sslmode=disable"}
 
 
 func CleanDB(x *xorm.Engine) {
 func CleanDB(x *xorm.Engine) {

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

@@ -78,11 +78,12 @@ func UpdateTeam(cmd *m.UpdateTeamCommand) error {
 	})
 	})
 }
 }
 
 
+// DeleteTeam will delete a team, its member and any permissions connected to the team
 func DeleteTeam(cmd *m.DeleteTeamCommand) error {
 func DeleteTeam(cmd *m.DeleteTeamCommand) error {
 	return inTransaction(func(sess *DBSession) error {
 	return inTransaction(func(sess *DBSession) error {
-		if res, err := sess.Query("SELECT 1 from team WHERE org_id=? and id=?", cmd.OrgId, cmd.Id); err != nil {
+		if teamExists, err := teamExists(cmd.OrgId, cmd.Id, sess); err != nil {
 			return err
 			return err
-		} else if len(res) != 1 {
+		} else if !teamExists {
 			return m.ErrTeamNotFound
 			return m.ErrTeamNotFound
 		}
 		}
 
 
@@ -102,6 +103,16 @@ func DeleteTeam(cmd *m.DeleteTeamCommand) error {
 	})
 	})
 }
 }
 
 
+func teamExists(orgId int64, teamId int64, sess *DBSession) (bool, error) {
+	if res, err := sess.Query("SELECT 1 from team WHERE org_id=? and id=?", orgId, teamId); err != nil {
+		return false, err
+	} else if len(res) != 1 {
+		return false, nil
+	}
+
+	return true, nil
+}
+
 func isTeamNameTaken(orgId int64, name string, existingId int64, sess *DBSession) (bool, error) {
 func isTeamNameTaken(orgId int64, name string, existingId int64, sess *DBSession) (bool, error) {
 	var team m.Team
 	var team m.Team
 	exists, err := sess.Where("org_id=? and name=?", orgId, name).Get(&team)
 	exists, err := sess.Where("org_id=? and name=?", orgId, name).Get(&team)
@@ -190,6 +201,7 @@ func GetTeamById(query *m.GetTeamByIdQuery) error {
 	return nil
 	return nil
 }
 }
 
 
+// GetTeamsByUser is used by the Guardian when checking a users' permissions
 func GetTeamsByUser(query *m.GetTeamsByUserQuery) error {
 func GetTeamsByUser(query *m.GetTeamsByUserQuery) error {
 	query.Result = make([]*m.Team, 0)
 	query.Result = make([]*m.Team, 0)
 
 
@@ -205,6 +217,7 @@ func GetTeamsByUser(query *m.GetTeamsByUserQuery) error {
 	return nil
 	return nil
 }
 }
 
 
+// AddTeamMember adds a user to a team
 func AddTeamMember(cmd *m.AddTeamMemberCommand) error {
 func AddTeamMember(cmd *m.AddTeamMemberCommand) error {
 	return inTransaction(func(sess *DBSession) error {
 	return inTransaction(func(sess *DBSession) error {
 		if res, err := sess.Query("SELECT 1 from team_member WHERE org_id=? and team_id=? and user_id=?", cmd.OrgId, cmd.TeamId, cmd.UserId); err != nil {
 		if res, err := sess.Query("SELECT 1 from team_member WHERE org_id=? and team_id=? and user_id=?", cmd.OrgId, cmd.TeamId, cmd.UserId); err != nil {
@@ -213,9 +226,9 @@ func AddTeamMember(cmd *m.AddTeamMemberCommand) error {
 			return m.ErrTeamMemberAlreadyAdded
 			return m.ErrTeamMemberAlreadyAdded
 		}
 		}
 
 
-		if res, err := sess.Query("SELECT 1 from team WHERE org_id=? and id=?", cmd.OrgId, cmd.TeamId); err != nil {
+		if teamExists, err := teamExists(cmd.OrgId, cmd.TeamId, sess); err != nil {
 			return err
 			return err
-		} else if len(res) != 1 {
+		} else if !teamExists {
 			return m.ErrTeamNotFound
 			return m.ErrTeamNotFound
 		}
 		}
 
 
@@ -232,18 +245,30 @@ func AddTeamMember(cmd *m.AddTeamMemberCommand) error {
 	})
 	})
 }
 }
 
 
+// RemoveTeamMember removes a member from a team
 func RemoveTeamMember(cmd *m.RemoveTeamMemberCommand) error {
 func RemoveTeamMember(cmd *m.RemoveTeamMemberCommand) error {
 	return inTransaction(func(sess *DBSession) error {
 	return inTransaction(func(sess *DBSession) error {
+		if teamExists, err := teamExists(cmd.OrgId, cmd.TeamId, sess); err != nil {
+			return err
+		} else if !teamExists {
+			return m.ErrTeamNotFound
+		}
+
 		var rawSql = "DELETE FROM team_member WHERE org_id=? and team_id=? and user_id=?"
 		var rawSql = "DELETE FROM team_member WHERE org_id=? and team_id=? and user_id=?"
-		_, err := sess.Exec(rawSql, cmd.OrgId, cmd.TeamId, cmd.UserId)
+		res, err := sess.Exec(rawSql, cmd.OrgId, cmd.TeamId, cmd.UserId)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
+		rows, err := res.RowsAffected()
+		if rows == 0 {
+			return m.ErrTeamMemberNotFound
+		}
 
 
 		return err
 		return err
 	})
 	})
 }
 }
 
 
+// GetTeamMembers return a list of members for the specified team
 func GetTeamMembers(query *m.GetTeamMembersQuery) error {
 func GetTeamMembers(query *m.GetTeamMembersQuery) error {
 	query.Result = make([]*m.TeamMemberDTO, 0)
 	query.Result = make([]*m.TeamMemberDTO, 0)
 	sess := x.Table("team_member")
 	sess := x.Table("team_member")

+ 7 - 4
pkg/services/sqlstore/team_test.go

@@ -84,13 +84,16 @@ func TestTeamCommandsAndQueries(t *testing.T) {
 			})
 			})
 
 
 			Convey("Should be able to remove users from a group", func() {
 			Convey("Should be able to remove users from a group", func() {
+				err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0]})
+				So(err, ShouldBeNil)
+
 				err = RemoveTeamMember(&m.RemoveTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0]})
 				err = RemoveTeamMember(&m.RemoveTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0]})
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
 
 
-				q1 := &m.GetTeamMembersQuery{TeamId: group1.Result.Id}
-				err = GetTeamMembers(q1)
+				q2 := &m.GetTeamMembersQuery{OrgId: testOrgId, TeamId: group1.Result.Id}
+				err = GetTeamMembers(q2)
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
-				So(len(q1.Result), ShouldEqual, 0)
+				So(len(q2.Result), ShouldEqual, 0)
 			})
 			})
 
 
 			Convey("Should be able to remove a group with users and permissions", func() {
 			Convey("Should be able to remove a group with users and permissions", func() {
@@ -99,7 +102,7 @@ func TestTeamCommandsAndQueries(t *testing.T) {
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
 				err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[2]})
 				err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[2]})
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
-				err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: testOrgId, Permission: m.PERMISSION_EDIT, TeamId: groupId})
+				err = testHelperUpdateDashboardAcl(1, m.DashboardAcl{DashboardId: 1, OrgId: testOrgId, Permission: m.PERMISSION_EDIT, TeamId: groupId})
 
 
 				err = DeleteTeam(&m.DeleteTeamCommand{OrgId: testOrgId, Id: groupId})
 				err = DeleteTeam(&m.DeleteTeamCommand{OrgId: testOrgId, Id: groupId})
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)

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

@@ -99,7 +99,7 @@ func TestUserDataAccess(t *testing.T) {
 				err = AddOrgUser(&m.AddOrgUserCommand{LoginOrEmail: users[0].Login, Role: m.ROLE_VIEWER, OrgId: users[0].OrgId})
 				err = AddOrgUser(&m.AddOrgUserCommand{LoginOrEmail: users[0].Login, Role: m.ROLE_VIEWER, OrgId: users[0].OrgId})
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
 
 
-				err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: users[0].OrgId, UserId: users[0].Id, Permission: m.PERMISSION_EDIT})
+				testHelperUpdateDashboardAcl(1, m.DashboardAcl{DashboardId: 1, OrgId: users[0].OrgId, UserId: users[0].Id, Permission: m.PERMISSION_EDIT})
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
 
 
 				err = SavePreferences(&m.SavePreferencesCommand{UserId: users[0].Id, OrgId: users[0].OrgId, HomeDashboardId: 1, Theme: "dark"})
 				err = SavePreferences(&m.SavePreferencesCommand{UserId: users[0].Id, OrgId: users[0].OrgId, HomeDashboardId: 1, Theme: "dark"})

+ 1 - 1
pkg/social/github_oauth.go

@@ -210,7 +210,7 @@ func (s *SocialGithub) UserInfo(client *http.Client, token *oauth2.Token) (*Basi
 	if err != nil {
 	if err != nil {
 		return nil, fmt.Errorf("Error getting user info: %s", err)
 		return nil, fmt.Errorf("Error getting user info: %s", err)
 	}
 	}
-	data.OrganizationsUrl = s.apiUrl + "/user/orgs"
+
 	userInfo := &BasicUserInfo{
 	userInfo := &BasicUserInfo{
 		Name:  data.Login,
 		Name:  data.Login,
 		Login: data.Login,
 		Login: data.Login,

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

@@ -98,11 +98,13 @@ func init() {
 		"AWS/SES":              {"Bounce", "Complaint", "Delivery", "Reject", "Send"},
 		"AWS/SES":              {"Bounce", "Complaint", "Delivery", "Reject", "Send"},
 		"AWS/SNS":              {"NumberOfMessagesPublished", "PublishSize", "NumberOfNotificationsDelivered", "NumberOfNotificationsFailed"},
 		"AWS/SNS":              {"NumberOfMessagesPublished", "PublishSize", "NumberOfNotificationsDelivered", "NumberOfNotificationsFailed"},
 		"AWS/SQS":              {"NumberOfMessagesSent", "SentMessageSize", "NumberOfMessagesReceived", "NumberOfEmptyReceives", "NumberOfMessagesDeleted", "ApproximateAgeOfOldestMessage", "ApproximateNumberOfMessagesDelayed", "ApproximateNumberOfMessagesVisible", "ApproximateNumberOfMessagesNotVisible"},
 		"AWS/SQS":              {"NumberOfMessagesSent", "SentMessageSize", "NumberOfMessagesReceived", "NumberOfEmptyReceives", "NumberOfMessagesDeleted", "ApproximateAgeOfOldestMessage", "ApproximateNumberOfMessagesDelayed", "ApproximateNumberOfMessagesVisible", "ApproximateNumberOfMessagesNotVisible"},
+		"AWS/States":           {"ExecutionTime", "ExecutionThrottled", "ExecutionsAborted", "ExecutionsFailed", "ExecutionsStarted", "ExecutionsSucceeded", "ExecutionsTimedOut", "ActivityRunTime", "ActivityScheduleTime", "ActivityTime", "ActivitiesFailed", "ActivitiesHeartbeatTimedOut", "ActivitiesScheduled", "ActivitiesScheduled", "ActivitiesSucceeded", "ActivitiesTimedOut", "LambdaFunctionRunTime", "LambdaFunctionScheduleTime", "LambdaFunctionTime", "LambdaFunctionsFailed", "LambdaFunctionsHeartbeatTimedOut", "LambdaFunctionsScheduled", "LambdaFunctionsStarted", "LambdaFunctionsSucceeded", "LambdaFunctionsTimedOut"},
 		"AWS/StorageGateway": {"CacheHitPercent", "CachePercentUsed", "CachePercentDirty", "CloudBytesDownloaded", "CloudDownloadLatency", "CloudBytesUploaded", "UploadBufferFree", "UploadBufferPercentUsed", "UploadBufferUsed", "QueuedWrites", "ReadBytes", "ReadTime", "TotalCacheSize", "WriteBytes", "WriteTime", "TimeSinceLastRecoveryPoint", "WorkingStorageFree", "WorkingStoragePercentUsed", "WorkingStorageUsed",
 		"AWS/StorageGateway": {"CacheHitPercent", "CachePercentUsed", "CachePercentDirty", "CloudBytesDownloaded", "CloudDownloadLatency", "CloudBytesUploaded", "UploadBufferFree", "UploadBufferPercentUsed", "UploadBufferUsed", "QueuedWrites", "ReadBytes", "ReadTime", "TotalCacheSize", "WriteBytes", "WriteTime", "TimeSinceLastRecoveryPoint", "WorkingStorageFree", "WorkingStoragePercentUsed", "WorkingStorageUsed",
 			"CacheHitPercent", "CachePercentUsed", "CachePercentDirty", "ReadBytes", "ReadTime", "WriteBytes", "WriteTime", "QueuedWrites"},
 			"CacheHitPercent", "CachePercentUsed", "CachePercentDirty", "ReadBytes", "ReadTime", "WriteBytes", "WriteTime", "QueuedWrites"},
 		"AWS/SWF": {"DecisionTaskScheduleToStartTime", "DecisionTaskStartToCloseTime", "DecisionTasksCompleted", "StartedDecisionTasksTimedOutOnClose", "WorkflowStartToCloseTime", "WorkflowsCanceled", "WorkflowsCompleted", "WorkflowsContinuedAsNew", "WorkflowsFailed", "WorkflowsTerminated", "WorkflowsTimedOut",
 		"AWS/SWF": {"DecisionTaskScheduleToStartTime", "DecisionTaskStartToCloseTime", "DecisionTasksCompleted", "StartedDecisionTasksTimedOutOnClose", "WorkflowStartToCloseTime", "WorkflowsCanceled", "WorkflowsCompleted", "WorkflowsContinuedAsNew", "WorkflowsFailed", "WorkflowsTerminated", "WorkflowsTimedOut",
 			"ActivityTaskScheduleToCloseTime", "ActivityTaskScheduleToStartTime", "ActivityTaskStartToCloseTime", "ActivityTasksCanceled", "ActivityTasksCompleted", "ActivityTasksFailed", "ScheduledActivityTasksTimedOutOnClose", "ScheduledActivityTasksTimedOutOnStart", "StartedActivityTasksTimedOutOnClose", "StartedActivityTasksTimedOutOnHeartbeat"},
 			"ActivityTaskScheduleToCloseTime", "ActivityTaskScheduleToStartTime", "ActivityTaskStartToCloseTime", "ActivityTasksCanceled", "ActivityTasksCompleted", "ActivityTasksFailed", "ScheduledActivityTasksTimedOutOnClose", "ScheduledActivityTasksTimedOutOnStart", "StartedActivityTasksTimedOutOnClose", "StartedActivityTasksTimedOutOnHeartbeat"},
 		"AWS/VPN":        {"TunnelState", "TunnelDataIn", "TunnelDataOut"},
 		"AWS/VPN":        {"TunnelState", "TunnelDataIn", "TunnelDataOut"},
+		"Rekognition":    {"SuccessfulRequestCount", "ThrottledCount", "ResponseTime", "DetectedFaceCount", "DetectedLabelCount", "ServerErrorCount", "UserErrorCount"},
 		"WAF":            {"AllowedRequests", "BlockedRequests", "CountedRequests"},
 		"WAF":            {"AllowedRequests", "BlockedRequests", "CountedRequests"},
 		"AWS/WorkSpaces": {"Available", "Unhealthy", "ConnectionAttempt", "ConnectionSuccess", "ConnectionFailure", "SessionLaunchTime", "InSessionLatency", "SessionDisconnect"},
 		"AWS/WorkSpaces": {"Available", "Unhealthy", "ConnectionAttempt", "ConnectionSuccess", "ConnectionFailure", "SessionLaunchTime", "InSessionLatency", "SessionDisconnect"},
 		"KMS":            {"SecondsUntilKeyMaterialExpiration"},
 		"KMS":            {"SecondsUntilKeyMaterialExpiration"},
@@ -145,9 +147,11 @@ func init() {
 		"AWS/SES":              {},
 		"AWS/SES":              {},
 		"AWS/SNS":              {"Application", "Platform", "TopicName"},
 		"AWS/SNS":              {"Application", "Platform", "TopicName"},
 		"AWS/SQS":              {"QueueName"},
 		"AWS/SQS":              {"QueueName"},
+		"AWS/States":           {"StateMachineArn", "ActivityArn", "LambdaFunctionArn"},
 		"AWS/StorageGateway":   {"GatewayId", "GatewayName", "VolumeId"},
 		"AWS/StorageGateway":   {"GatewayId", "GatewayName", "VolumeId"},
 		"AWS/SWF":              {"Domain", "WorkflowTypeName", "WorkflowTypeVersion", "ActivityTypeName", "ActivityTypeVersion"},
 		"AWS/SWF":              {"Domain", "WorkflowTypeName", "WorkflowTypeVersion", "ActivityTypeName", "ActivityTypeVersion"},
 		"AWS/VPN":              {"VpnId", "TunnelIpAddress"},
 		"AWS/VPN":              {"VpnId", "TunnelIpAddress"},
+		"Rekognition":          {},
 		"WAF":                  {"Rule", "WebACL"},
 		"WAF":                  {"Rule", "WebACL"},
 		"AWS/WorkSpaces":       {"DirectoryId", "WorkspaceId"},
 		"AWS/WorkSpaces":       {"DirectoryId", "WorkspaceId"},
 		"KMS":                  {"KeyId"},
 		"KMS":                  {"KeyId"},

部分文件因为文件数量过多而无法显示