Merge remote-tracking branch 'upstream/master' into postgres-query-builder

This commit is contained in:
Sven Klemm 2018-02-21 08:24:04 +01:00
commit c5de5ed5be
193 changed files with 5621 additions and 2186 deletions

View File

@ -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,

View File

@ -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)

View File

@ -80,8 +80,11 @@ In your custom.ini uncomment (remove the leading `;`) sign. And set `app_mode =
### Running tests ### Running tests
- You can run backend Golang tests using "go test ./pkg/...". #### Frontend
- Execute all frontend tests with "npm run test" Execute all frontend tests
```bash
npm run test
```
Writing & watching frontend tests (we have two test runners) Writing & watching frontend tests (we have two test runners)
@ -92,6 +95,18 @@ Writing & watching frontend tests (we have two test runners)
- Start watcher: `npm run karma` - Start watcher: `npm run karma`
- Karma+Mocha runs all files that end with the name "_specs.ts". - Karma+Mocha runs all files that end with the name "_specs.ts".
#### Backend
```bash
# Run Golang tests using sqlite3 as database (default)
go test ./pkg/...
# Run Golang tests using mysql as database - convenient to use /docker/blocks/mysql_tests
GRAFANA_TEST_DB=mysql go test ./pkg/...
# Run Golang tests using postgres as database - convenient to use /docker/blocks/postgres_tests
GRAFANA_TEST_DB=postgres go test ./pkg/...
```
## Contribute ## Contribute
If you have any idea for an improvement or found a bug, do not hesitate to open an issue. If you have any idea for an improvement or found a bug, do not hesitate to open an issue.

View File

@ -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 =

View File

@ -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)"

View File

@ -1,6 +1,10 @@
# # config file version
apiVersion: 1
#providers:
# - name: 'default' # - name: 'default'
# org_id: 1 # orgId: 1
# folder: '' # folder: ''
# type: file # type: file
# options: # options:
# folder: /var/lib/grafana/dashboards # path: /var/lib/grafana/dashboards

View File

@ -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 # # <int> org id. will default to orgId 1 if not specified
# org_id: 1 # 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: "..."

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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 # <int> org id. will default to orgId 1 if not specified
org_id: 1 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,17 +172,28 @@ 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:
folder: /var/lib/grafana/dashboards path: /var/lib/grafana/dashboards
``` ```
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. 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.

View File

@ -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

View File

@ -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.

View File

@ -83,7 +83,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 ## Get dashboard

View File

@ -1,49 +1,107 @@
+++ +++
title = "Docs Home" title = "Grafana documentation"
description = "Install guide for Grafana" description = "Guides, Installation & Feature Documentation"
keywords = ["grafana", "installation", "documentation"] keywords = ["grafana", "installation", "documentation"]
type = "docs" type = "docs"
aliases = ["v1.1", "guides/reference/admin"] aliases = ["v1.1", "guides/reference/admin"]
+++ +++
# Welcome to the Grafana Documentation # Grafana Documentation
Grafana is an open source metric analytics & visualization suite. It is most commonly used for <h2>Installing Grafana</h2>
visualizing time series data for infrastructure and application analytics but many use it in <div class="nav-cards">
other domains including industrial sensors, home automation, weather, and process control. <a href="{{< relref "installation/debian.md" >}}" class="nav-cards__item nav-cards__item--install">
<div class="nav-cards__icon fa fa-linux">
</div>
<h5>Installing on Linux</h5>
</a>
<a href="{{< relref "installation/mac.md" >}}" class="nav-cards__item nav-cards__item--install">
<div class="nav-cards__icon fa fa-apple">
</div>
<h5>Installing on Mac OS X</h5>
</a>
<a href="{{< relref "installation/windows.md" >}}" class="nav-cards__item nav-cards__item--install">
<div class="nav-cards__icon fa fa-windows">
</div>
<h5>Installing on Windows</h5>
</a>
<a href="https://grafana.com/cloud/grafana" class="nav-cards__item nav-cards__item--install">
<div class="nav-cards__icon fa fa-cloud">
</div>
<h5>Grafana Cloud</h5>
</a>
<a href="https://grafana.com/grafana/download" class="nav-cards__item nav-cards__item--install">
<div class="nav-cards__icon fa fa-moon-o">
</div>
<h5>Nightly Builds</h5>
</a>
<div class="nav-cards__item nav-cards__item--install">
<h5>For other platforms Read the <a href="{{< relref "project/building_from_source.md" >}}">build from source</a>
instructions for more information.</h5>
</div>
</div>
## Installing Grafana <h2>Guides</h2>
- [Installing on Debian / Ubuntu](installation/debian)
- [Installing on RPM-based Linux (CentOS, Fedora, OpenSuse, RedHat)](installation/rpm)
- [Installing on Mac OS X](installation/mac)
- [Installing on Windows](installation/windows)
- [Installing on Docker](installation/docker)
- [Installing using Provisioning (Chef, Puppet, Salt, Ansible, etc)](administration/provisioning#configuration-management-tools)
- [Nightly Builds](https://grafana.com/grafana/download)
For other platforms Read the [build from source]({{< relref "project/building_from_source.md" >}}) <div class="nav-cards">
instructions for more information. <a href="https://grafana.com/grafana" class="nav-cards__item nav-cards__item--guide">
<h4>What is Grafana?</h4>
<p>Grafana feature highlights.</p>
</a>
<a href="{{< relref "installation/configuration.md" >}}" class="nav-cards__item nav-cards__item--guide">
<h4>Configure Grafana</h4>
<p>Article on all the Grafana configuration and setup options.</p>
</a>
<a href="{{< relref "guides/getting_started.md" >}}" class="nav-cards__item nav-cards__item--guide">
<h4>Getting Started</h4>
<p>A guide that walks you through the basics of using Grafana</p>
</a>
<a href="{{< relref "administration/provisioning.md" >}}" class="nav-cards__item nav-cards__item--guide">
<h4>Provisioning</h4>
<p>A guide to help you automate your Grafana setup & configuration.</p>
</a>
<a href="{{< relref "guides/whats-new-in-v5.md" >}}" class="nav-cards__item nav-cards__item--guide">
<h4>What's new in v5.0</h4>
<p>Article on all the new cool features and enhancements in v5.0</p>
</a>
<a href="{{< relref "tutorials/screencasts.md" >}}" class="nav-cards__item nav-cards__item--guide">
<h4>Screencasts</h4>
<p>Video tutorials & guides</p>
</a>
</div>
## Configuring Grafana <h2>Data Source Guides</h2>
<div class="nav-cards">
The back-end web server has a number of configuration options. Go the <a href="{{< relref "features/datasources/graphite.md" >}}" class="nav-cards__item nav-cards__item--ds">
[Configuration]({{< relref "installation/configuration.md" >}}) page for details on all <img src="/img/docs/logos/icon_graphite.svg" >
those options. <h5>Graphite</h5>
</a>
<a href="{{< relref "features/datasources/elasticsearch.md" >}}" class="nav-cards__item nav-cards__item--ds">
## Getting Started <img src="/img/docs/logos/icon_elasticsearch.svg" >
<h5>Elasticsearch</h5>
- [Getting Started]({{< relref "guides/getting_started.md" >}}) </a>
- [Basic Concepts]({{< relref "guides/basic_concepts.md" >}}) <a href="{{< relref "features/datasources/influxdb.md" >}}" class="nav-cards__item nav-cards__item--ds">
- [Screencasts]({{< relref "tutorials/screencasts.md" >}}) <img src="/img/docs/logos/icon_influxdb.svg" >
<h5>InfluxDB</h5>
## Data Source Guides </a>
<a href="{{< relref "features/datasources/prometheus.md" >}}" class="nav-cards__item nav-cards__item--ds">
- [Graphite]({{< relref "features/datasources/graphite.md" >}}) <img src="/img/docs/logos/icon_prometheus.svg" >
- [Elasticsearch]({{< relref "features/datasources/elasticsearch.md" >}}) <h5>Prometheus</h5>
- [InfluxDB]({{< relref "features/datasources/influxdb.md" >}}) </a>
- [Prometheus]({{< relref "features/datasources/prometheus.md" >}}) <a href="{{< relref "features/datasources/opentsdb.md" >}}" class="nav-cards__item nav-cards__item--ds">
- [OpenTSDB]({{< relref "features/datasources/opentsdb.md" >}}) <img src="/img/docs/logos/icon_opentsdb.png" >
- [MySQL]({{< relref "features/datasources/mysql.md" >}}) <h5>OpenTSDB</h5>
- [Postgres]({{< relref "features/datasources/postgres.md" >}}) </a>
- [Cloudwatch]({{< relref "features/datasources/cloudwatch.md" >}}) <a href="{{< relref "features/datasources/mysql.md" >}}" class="nav-cards__item nav-cards__item--ds">
<img src="/img/docs/logos/icon_mysql.png" >
<h5>MySQL</h5>
</a>
<a href="{{< relref "features/datasources/postgres.md" >}}" class="nav-cards__item nav-cards__item--ds">
<img src="/img/docs/logos/icon_postgres.svg" >
<h5>Postgres</h5>
</a>
<a href="{{< relref "features/datasources/cloudwatch.md" >}}" class="nav-cards__item nav-cards__item--ds">
<img src="/img/docs/logos/icon_cloudwatch.svg">
<h5>Cloudwatch</h5>
</a>
</div>

View File

@ -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

View File

@ -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)"

View File

@ -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`.

View File

@ -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.

View File

@ -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",

View File

@ -1,6 +1,6 @@
#! /usr/bin/env bash #! /usr/bin/env bash
deb_ver=5.0.0-beta1 deb_ver=5.0.0-beta4
rpm_ver=5.0.0-beta1 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

View File

@ -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) for _, alert := range query.Result {
if resp != nil { alert.Url = models.GetDashboardUrl(alert.DashboardUid, alert.DashboardSlug)
return resp
} }
return Json(200, alertDTOs) return Json(200, query.Result)
}
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
} }
// 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)

View File

@ -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
} }

View File

@ -150,13 +150,13 @@ func (hs *HttpServer) registerRoutes() {
apiRoute.Group("/teams", func(teamsRoute RouteRegister) { apiRoute.Group("/teams", func(teamsRoute RouteRegister) {
teamsRoute.Get("/:teamId", wrap(GetTeamById)) teamsRoute.Get("/:teamId", wrap(GetTeamById))
teamsRoute.Get("/search", wrap(SearchTeams)) teamsRoute.Get("/search", wrap(SearchTeams))
teamsRoute.Post("/", quota("teams"), reqOrgAdmin, bind(m.CreateTeamCommand{}), wrap(CreateTeam)) teamsRoute.Post("/", quota("teams"), bind(m.CreateTeamCommand{}), wrap(CreateTeam))
teamsRoute.Put("/:teamId", reqOrgAdmin, bind(m.UpdateTeamCommand{}), wrap(UpdateTeam)) teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), wrap(UpdateTeam))
teamsRoute.Delete("/:teamId", reqOrgAdmin, wrap(DeleteTeamById)) teamsRoute.Delete("/:teamId", wrap(DeleteTeamById))
teamsRoute.Get("/:teamId/members", reqOrgAdmin, wrap(GetTeamMembers)) teamsRoute.Get("/:teamId/members", wrap(GetTeamMembers))
teamsRoute.Post("/:teamId/members", reqOrgAdmin, quota("teams"), bind(m.AddTeamMemberCommand{}), wrap(AddTeamMember)) teamsRoute.Post("/:teamId/members", quota("teams"), bind(m.AddTeamMemberCommand{}), wrap(AddTeamMember))
teamsRoute.Delete("/:teamId/members/:userId", reqOrgAdmin, wrap(RemoveTeamMember)) teamsRoute.Delete("/:teamId/members/:userId", wrap(RemoveTeamMember))
}) }, reqOrgAdmin)
// org information available to all users. // org information available to all users.
apiRoute.Group("/org", func(orgRoute RouteRegister) { apiRoute.Group("/org", func(orgRoute RouteRegister) {
@ -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))
}) })
}) })
}) })

View File

@ -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 dash.Id == 0 && dash.Uid == "" {
// 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 {
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)
}

View File

@ -13,7 +13,12 @@ import (
func GetDashboardAclList(c *middleware.Context) Response { func GetDashboardAclList(c *middleware.Context) Response {
dashId := c.ParamsInt64(":dashboardId") dashId := c.ParamsInt64(":dashboardId")
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser) _, rsp := getDashboardHelper(c.OrgId, "", dashId, "")
if rsp != nil {
return rsp
}
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)
@ -36,7 +41,12 @@ func GetDashboardAclList(c *middleware.Context) Response {
func UpdateDashboardAcl(c *middleware.Context, apiCmd dtos.UpdateDashboardAclCommand) Response { func UpdateDashboardAcl(c *middleware.Context, apiCmd dtos.UpdateDashboardAclCommand) Response {
dashId := c.ParamsInt64(":dashboardId") dashId := c.ParamsInt64(":dashboardId")
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser) _, rsp := getDashboardHelper(c.OrgId, "", dashId, "")
if rsp != nil {
return rsp
}
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)
} }
@ -74,28 +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")
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, "")
}

View File

@ -15,14 +15,22 @@ 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}, {OrgId: 1, DashboardId: 1, UserId: 2, Permission: m.PERMISSION_VIEW},
{Id: 2, OrgId: 1, DashboardId: 1, UserId: 3, Permission: m.PERMISSION_EDIT}, {OrgId: 1, DashboardId: 1, UserId: 3, Permission: m.PERMISSION_EDIT},
{Id: 3, OrgId: 1, DashboardId: 1, UserId: 4, Permission: m.PERMISSION_ADMIN}, {OrgId: 1, DashboardId: 1, UserId: 4, Permission: m.PERMISSION_ADMIN},
{Id: 4, OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_VIEW}, {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, TeamId: 2, Permission: m.PERMISSION_ADMIN},
} }
dtoRes := transformDashboardAclsToDTOs(mockResult) dtoRes := transformDashboardAclsToDTOs(mockResult)
getDashboardQueryResult := m.NewDashboard("Dash")
var getDashboardNotFoundError error
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
query.Result = getDashboardQueryResult
return getDashboardNotFoundError
})
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = dtoRes query.Result = dtoRes
return nil return nil
@ -60,11 +68,35 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
So(respJSON.GetIndex(0).Get("permission").MustInt(), ShouldEqual, m.PERMISSION_VIEW) So(respJSON.GetIndex(0).Get("permission").MustInt(), ShouldEqual, m.PERMISSION_VIEW)
}) })
}) })
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_ADMIN, func(sc *scenarioContext) {
getDashboardNotFoundError = m.ErrDashboardNotFound
sc.handlerFunc = GetDashboardAclList
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
Convey("Should not be able to access ACL", func() {
So(sc.resp.Code, ShouldEqual, 404)
})
})
Convey("Should not be able to update permissions for non-existing dashboard", func() {
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
{UserId: 1000, Permission: m.PERMISSION_ADMIN},
},
}
postAclScenario("When calling POST on", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_ADMIN, cmd, func(sc *scenarioContext) {
getDashboardNotFoundError = m.ErrDashboardNotFound
CallPostAcl(sc)
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
@ -74,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{
@ -112,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)
@ -128,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() {
@ -165,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() {
@ -192,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)
})
})
}) })
}) })
} }
@ -215,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,

View File

@ -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 { // This tests three main scenarios.
inserted []*dashboards.SaveDashboardItem // If a user has access to execute an action on a dashboard:
getDashboard []*m.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 (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
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.SignedInUser = &m.SignedInUser{OrgId: cmd.OrgId, UserId: cmd.UserId}
sc.context.OrgId = TestOrgID
sc.context.OrgRole = role
return PostDashboard(c, cmd) return PostDashboard(c, cmd)
}) })
fakeRepo = &fakeDashboardRepo{} origNewDashboardService := dashboards.NewService
dashboards.SetRepository(fakeRepo) dashboards.MockDashboardService(mock)
sc.m.Post(routePattern, sc.defaultHandler) sc.m.Post(routePattern, sc.defaultHandler)
defer func() {
dashboards.NewService = origNewDashboardService
}()
fn(sc) fn(sc)
}) })
} }

View File

@ -46,26 +46,30 @@ func addOrgUserHelper(cmd m.AddOrgUserCommand) Response {
// GET /api/org/users // GET /api/org/users
func GetOrgUsersForCurrentOrg(c *middleware.Context) Response { func GetOrgUsersForCurrentOrg(c *middleware.Context) Response {
return getOrgUsersHelper(c.OrgId) return getOrgUsersHelper(c.OrgId, c.Params("query"), c.ParamsInt("limit"))
} }
// GET /api/orgs/:orgId/users // GET /api/orgs/:orgId/users
func GetOrgUsers(c *middleware.Context) Response { func GetOrgUsers(c *middleware.Context) Response {
return getOrgUsersHelper(c.ParamsInt64(":orgId")) return getOrgUsersHelper(c.ParamsInt64(":orgId"), "", 0)
} }
func getOrgUsersHelper(orgId int64) Response { func getOrgUsersHelper(orgId int64, query string, limit int) Response {
query := m.GetOrgUsersQuery{OrgId: orgId} q := m.GetOrgUsersQuery{
OrgId: orgId,
Query: query,
Limit: limit,
}
if err := bus.Dispatch(&query); err != nil { if err := bus.Dispatch(&q); err != nil {
return ApiError(500, "Failed to get account user", err) return ApiError(500, "Failed to get account user", err)
} }
for _, user := range query.Result { for _, user := range q.Result {
user.AvatarUrl = dtos.GetGravatarUrl(user.Email) user.AvatarUrl = dtos.GetGravatarUrl(user.Email)
} }
return Json(200, query.Result) return Json(200, q.Result)
} }
// PATCH /api/org/users/:userId // PATCH /api/org/users/:userId

View File

@ -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,

View File

@ -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)

View File

@ -26,6 +26,7 @@ func CreateTeam(c *middleware.Context, cmd m.CreateTeamCommand) Response {
// PUT /api/teams/:teamId // PUT /api/teams/:teamId
func UpdateTeam(c *middleware.Context, cmd m.UpdateTeamCommand) Response { func UpdateTeam(c *middleware.Context, cmd m.UpdateTeamCommand) Response {
cmd.OrgId = c.OrgId
cmd.Id = c.ParamsInt64(":teamId") cmd.Id = c.ParamsInt64(":teamId")
if err := bus.Dispatch(&cmd); err != nil { if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrTeamNameTaken { if err == m.ErrTeamNameTaken {
@ -39,7 +40,7 @@ func UpdateTeam(c *middleware.Context, cmd m.UpdateTeamCommand) Response {
// DELETE /api/teams/:teamId // DELETE /api/teams/:teamId
func DeleteTeamById(c *middleware.Context) Response { func DeleteTeamById(c *middleware.Context) Response {
if err := bus.Dispatch(&m.DeleteTeamCommand{Id: c.ParamsInt64(":teamId")}); err != nil { if err := bus.Dispatch(&m.DeleteTeamCommand{OrgId: c.OrgId, Id: c.ParamsInt64(":teamId")}); err != nil {
if err == m.ErrTeamNotFound { if err == m.ErrTeamNotFound {
return ApiError(404, "Failed to delete Team. ID not found", nil) return ApiError(404, "Failed to delete Team. ID not found", nil)
} }
@ -60,11 +61,11 @@ func SearchTeams(c *middleware.Context) Response {
} }
query := m.SearchTeamsQuery{ query := m.SearchTeamsQuery{
OrgId: c.OrgId,
Query: c.Query("query"), Query: c.Query("query"),
Name: c.Query("name"), Name: c.Query("name"),
Page: page, Page: page,
Limit: perPage, Limit: perPage,
OrgId: c.OrgId,
} }
if err := bus.Dispatch(&query); err != nil { if err := bus.Dispatch(&query); err != nil {
@ -83,7 +84,7 @@ func SearchTeams(c *middleware.Context) Response {
// GET /api/teams/:teamId // GET /api/teams/:teamId
func GetTeamById(c *middleware.Context) Response { func GetTeamById(c *middleware.Context) Response {
query := m.GetTeamByIdQuery{Id: c.ParamsInt64(":teamId")} query := m.GetTeamByIdQuery{OrgId: c.OrgId, Id: c.ParamsInt64(":teamId")}
if err := bus.Dispatch(&query); err != nil { if err := bus.Dispatch(&query); err != nil {
if err == m.ErrTeamNotFound { if err == m.ErrTeamNotFound {

View File

@ -10,7 +10,7 @@ import (
// GET /api/teams/:teamId/members // GET /api/teams/:teamId/members
func GetTeamMembers(c *middleware.Context) Response { func GetTeamMembers(c *middleware.Context) Response {
query := m.GetTeamMembersQuery{TeamId: c.ParamsInt64(":teamId")} query := m.GetTeamMembersQuery{OrgId: c.OrgId, TeamId: c.ParamsInt64(":teamId")}
if err := bus.Dispatch(&query); err != nil { if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to get Team Members", err) return ApiError(500, "Failed to get Team Members", err)
@ -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.ErrTeamMemberAlreadyAdded { if err == m.ErrTeamNotFound {
return ApiError(400, "User is already added to this team", err) return ApiError(404, "Team not found", nil)
} }
if err == m.ErrTeamMemberAlreadyAdded {
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)
} }
@ -42,7 +47,15 @@ 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{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")

View File

@ -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)
} }

View File

@ -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")
} }

View File

@ -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)
} }

View File

@ -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
}

View File

@ -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
// //

View File

@ -13,22 +13,26 @@ import (
// Typed errors // Typed errors
var ( var (
ErrDashboardNotFound = errors.New("Dashboard not found") ErrDashboardNotFound = errors.New("Dashboard not found")
ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found") ErrFolderNotFound = errors.New("Folder not found")
ErrDashboardWithSameUIDExists = errors.New("A dashboard with the same uid already exists") ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
ErrDashboardWithSameNameInFolderExists = errors.New("A dashboard with the same name in the folder already exists") ErrDashboardWithSameUIDExists = errors.New("A dashboard with the same uid already exists")
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else") ErrDashboardWithSameNameInFolderExists = errors.New("A dashboard with the same name in the folder already exists")
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty") ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder") ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard") ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder")
ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data") ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard")
ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists") ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data")
ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard id") ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists")
ErrDashboardExistingCannotChangeToDashboard = errors.New("An existing folder cannot be changed to a dashboard") ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard id")
ErrDashboardTypeMismatch = errors.New("Dashboard cannot be changed to a folder") ErrDashboardTypeMismatch = errors.New("Dashboard cannot be changed to a folder")
ErrDashboardFolderWithSameNameAsDashboard = errors.New("Folder name cannot be the same as one of its dashboards") 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") ErrDashboardWithSameNameAsFolder = errors.New("Dashboard name cannot be the same as folder")
RootFolderName = "General" 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"`

View File

@ -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_ES: true,
DS_GRAPHITE: true, DS_GRAPHITE: true,
DS_INFLUXDB: true, DS_INFLUXDB: true,
DS_INFLUXDB_08: true, DS_INFLUXDB_08: true,
DS_KAIROSDB: true, DS_KAIROSDB: true,
DS_CLOUDWATCH: true, DS_CLOUDWATCH: true,
DS_PROMETHEUS: true, DS_PROMETHEUS: true,
DS_OPENTSDB: true, DS_OPENTSDB: true,
DS_POSTGRES: true, DS_POSTGRES: true,
DS_MYSQL: true, DS_MYSQL: true,
"opennms": true, "opennms": true,
"druid": true, "abhisant-druid-datasource": true,
"dalmatinerdb": true, "dalmatinerdb-datasource": true,
"gnocci": true, "gnocci": true,
"zabbix": 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,

View File

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

View File

@ -95,7 +95,10 @@ type UpdateOrgUserCommand struct {
// QUERIES // QUERIES
type GetOrgUsersQuery struct { type GetOrgUsersQuery struct {
OrgId int64 OrgId int64
Query string
Limit int
Result []*OrgUserDTO Result []*OrgUserDTO
} }

View File

@ -7,8 +7,9 @@ import (
// Typed errors // Typed errors
var ( var (
ErrTeamNotFound = errors.New("Team not found") ErrTeamNotFound = errors.New("Team not found")
ErrTeamNameTaken = errors.New("Team name is taken") ErrTeamNameTaken = errors.New("Team name is taken")
ErrTeamMemberNotFound = errors.New("Team member not found")
) )
// Team model // Team model
@ -37,18 +38,22 @@ type UpdateTeamCommand struct {
Id int64 Id int64
Name string Name string
Email string Email string
OrgId int64 `json:"-"`
} }
type DeleteTeamCommand struct { type DeleteTeamCommand struct {
Id int64 OrgId int64
Id int64
} }
type GetTeamByIdQuery struct { type GetTeamByIdQuery struct {
OrgId int64
Id int64 Id int64
Result *Team Result *Team
} }
type GetTeamsByUserQuery struct { type GetTeamsByUserQuery struct {
OrgId int64
UserId int64 `json:"userId"` UserId int64 `json:"userId"`
Result []*Team `json:"teams"` Result []*Team `json:"teams"`
} }

View File

@ -31,6 +31,7 @@ type AddTeamMemberCommand struct {
} }
type RemoveTeamMemberCommand struct { type RemoveTeamMemberCommand struct {
OrgId int64 `json:"-"`
UserId int64 UserId int64
TeamId int64 TeamId int64
} }
@ -39,6 +40,7 @@ type RemoveTeamMemberCommand struct {
// QUERIES // QUERIES
type GetTeamMembersQuery struct { type GetTeamMembersQuery struct {
OrgId int64
TeamId int64 TeamId int64
Result []*TeamMemberDTO Result []*TeamMemberDTO
} }

View File

@ -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), Revision: savedDash.Data.Get("revision").MustInt64(1),
ImportedUri: "db/" + saveCmd.Result.Slug, ImportedUri: "db/" + savedDash.Slug,
ImportedUrl: saveCmd.Result.GetUrl(), ImportedUrl: savedDash.GetUrl(),
ImportedRevision: dashboard.Data.Get("revision").MustInt64(1), ImportedRevision: dashboard.Data.Get("revision").MustInt64(1),
Imported: true, Imported: true,
} }

View File

@ -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 origNewDashboardService := dashboards.NewService
mock := &dashboards.FakeDashboardService{}
bus.AddHandler("test", func(cmd *m.SaveDashboardCommand) error { dashboards.MockDashboardService(mock)
importedDash = cmd.GetDashboardModel()
cmd.Result = importedDash
return nil
})
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()) {

View File

@ -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,
} }

View File

@ -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,

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 { // New factory for creating a new dashboard guardian instance
return &DashboardGuardian{ 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) { func (g *dashboardGuardianImpl) CheckPermissionBeforeUpdate(permission m.PermissionType, updatePermissions []*m.DashboardAcl) (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) {
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,12 +147,12 @@ 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
} }
query := m.GetTeamsByUserQuery{UserId: g.user.UserId} query := m.GetTeamsByUserQuery{OrgId: g.orgId, UserId: g.user.UserId}
err := bus.Dispatch(&query) err := bus.Dispatch(&query)
g.groups = query.Result g.groups = query.Result

View File

@ -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())) parsedDashboards, err := cr.parseConfigs(file)
yamlFile, err := ioutil.ReadFile(filename)
if err != nil { if err != nil {
return nil, err
} }
var dashCfg []*DashboardsAsConfig if len(parsedDashboards) > 0 {
err = yaml.Unmarshal(yamlFile, &dashCfg) dashboards = append(dashboards, parsedDashboards...)
if err != nil {
return nil, err
} }
dashboards = append(dashboards, dashCfg...)
} }
for i := range dashboards { for i := range dashboards {

View File

@ -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() { Convey("Can read config file version 1 format", func() {
cfgProvider := configReader{path: simpleDashboardConfig, log: logger}
cfgProvider := configReader{path: simpleDashboardConfig, log: log.New("test-logger")}
cfg, err := cfgProvider.readConfig() cfg, err := cfgProvider.readConfig()
if err != nil { So(err, ShouldBeNil)
t.Fatalf("readConfig return an error %v", err)
}
So(len(cfg), ShouldEqual, 2) validateDashboardAsConfig(cfg)
})
ds := cfg[0] Convey("Can read config file in version 0 format", func() {
cfgProvider := configReader{path: oldVersion, log: logger}
cfg, err := cfgProvider.readConfig()
So(err, ShouldBeNil)
So(ds.Name, ShouldEqual, "general dashboards") validateDashboardAsConfig(cfg)
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]
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")
}) })
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)
}

View File

@ -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
}

View File

@ -25,12 +25,10 @@ var (
) )
type fileReader struct { type fileReader struct {
Cfg *DashboardsAsConfig Cfg *DashboardsAsConfig
Path string Path string
log log.Logger log log.Logger
dashboardRepo dashboards.Repository dashboardService dashboards.DashboardProvisioningService
cache *dashboardCache
createWalk func(fr *fileReader, folderId int64) filepath.WalkFunc
} }
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, Cfg: cfg,
Path: path, Path: path,
log: log, log: log,
dashboardRepo: dashboards.GetRepository(), dashboardService: dashboards.NewProvisioningService(),
cache: NewDashboardCache(),
createWalk: createWalkFn,
}, 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 := &dashboards.SaveDashboardDTO{}
dash.Dashboard = models.NewDashboard(cfg.Folder) 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 { 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 {
return nil, err
}
return fi, nil
}
return fileinfo, err
}
func createWalkFn(filesOnDisk map[string]os.FileInfo) filepath.WalkFunc {
return func(path string, fileInfo os.FileInfo, err error) error { return func(path string, fileInfo os.FileInfo, err error) error {
if err != nil { if err != nil {
return err return err
} }
if fileInfo.IsDir() {
if strings.HasPrefix(fileInfo.Name(), ".") {
return filepath.SkipDir
}
return nil
}
if !strings.HasSuffix(fileInfo.Name(), ".json") { isValid, err := validateWalkablePath(fileInfo)
return nil if !isValid {
}
checkFilepath, err := filepath.EvalSymlinks(path)
if path != checkFilepath {
path = checkFilepath
fi, err := os.Lstat(checkFilepath)
if err != nil {
return err
}
fileInfo = fi
}
cachedDashboard, exist := fr.cache.getCache(path)
if exist && cachedDashboard.UpdatedAt == fileInfo.ModTime() {
return nil
}
dash, err := fr.readDashboardFromFile(path, folderId)
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
}
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)
return err return err
} }
if err != nil { filesOnDisk[path] = fileInfo
fr.log.Error("failed to query for dashboard", "slug", dash.Dashboard.Slug, "error", err) return nil
return nil
}
// break if db version is newer then fil version
if cmd.Result.Updated.Unix() >= fileInfo.ModTime().Unix() {
return nil
}
fr.log.Debug("loading dashboard from disk into database.", "file", path)
_, err = fr.dashboardRepo.SaveDashboard(dash)
return err
} }
} }
func (fr *fileReader) readDashboardFromFile(path string, folderId int64) (*dashboards.SaveDashboardItem, error) { func validateWalkablePath(fileInfo os.FileInfo) (bool, error) {
if fileInfo.IsDir() {
if strings.HasPrefix(fileInfo.Name(), ".") {
return false, filepath.SkipDir
}
return false, nil
}
if !strings.HasSuffix(fileInfo.Name(), ".json") {
return false, nil
}
return true, nil
}
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
}
fr.cache.addDashboardCache(path, dash)
return dash, nil 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
}
}
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)
}
}
}

View File

@ -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)
}) So(dashboards, ShouldEqual, 2)
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)
}) })
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{ noFiles := map[string]os.FileInfo{}
Name: "Default",
Type: "file",
OrgId: 1,
Folder: "",
Options: map[string]interface{}{
"path": defaultDashboards,
},
}
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"))
So(err, ShouldBeNil)
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 { func mockDashboardProvisioningService() *fakeDashboardProvisioningService {
inserted []*dashboards.SaveDashboardItem 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) { func (s *fakeDashboardProvisioningService) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) {
repo.inserted = append(repo.inserted, json) return s.provisioned, nil
return json.Dashboard, 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

View File

@ -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

View File

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

View File

@ -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

View File

@ -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
} }

View File

@ -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
} }

View File

@ -10,16 +10,45 @@ import (
) )
type DashboardsAsConfig struct { type DashboardsAsConfig struct {
Name string `json:"name" yaml:"name"` Name string
Type string `json:"type" yaml:"type"` Type string
OrgId int64 `json:"org_id" yaml:"org_id"` OrgId int64
Folder string `json:"folder" yaml:"folder"` Folder string
Editable bool `json:"editable" yaml:"editable"` Editable bool
Options map[string]interface{} `json:"options" yaml:"options"` Options map[string]interface{}
DisableDeletion bool
} }
func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *DashboardsAsConfig, folderId int64) (*dashboards.SaveDashboardItem, error) { type DashboardsAsConfigV0 struct {
dash := &dashboards.SaveDashboardItem{} 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
}

View File

@ -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
}

View File

@ -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(dsCfg.ApiVersion, ShouldEqual, 1)
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) validateDatasource(dsCfg)
So(ds.JsonData["graphiteVersion"], ShouldEqual, "1.1") validateDeleteDatasources(dsCfg)
So(ds.JsonData["tlsAuth"], ShouldEqual, true)
So(ds.JsonData["tlsAuthWithCACert"], ShouldEqual, true)
So(len(ds.SecureJsonData), ShouldBeGreaterThan, 2) dsCount := 0
So(ds.SecureJsonData["tlsCACert"], ShouldEqual, "MjNOcW9RdkbUDHZmpco2HCYzVq9dE+i6Yi+gmUJotq5CDA==") delDsCount := 0
So(ds.SecureJsonData["tlsClientCert"], ShouldEqual, "ckN0dGlyMXN503YNfjTcf9CV+GGQneN+xmAclQ==")
So(ds.SecureJsonData["tlsClientKey"], ShouldEqual, "ZkN4aG1aNkja/gKAB1wlnKFIsy2SRDq4slrM0A==")
dstwo := cfg[1].Datasources[0] for _, c := range cfg {
So(dstwo.Name, ShouldEqual, "name2") 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

View File

@ -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
}

View File

@ -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 basicAuth: true
basic_auth_user: basic_auth_user basicAuthUser: basic_auth_user
basic_auth_password: basic_auth_password basicAuthPassword: basic_auth_password
with_credentials: true withCredentials: true
is_default: true isDefault: true
json_data: 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"` ApiVersion int64
DeleteDatasources []*DeleteDatasourceConfig `json:"delete_datasources" yaml:"delete_datasources"`
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 { type DeleteDatasourceConfigV1 struct {
OrgId int64 `json:"org_id" yaml:"org_id"` OrgId int64 `json:"orgId" yaml:"orgId"`
Version int `json:"version" yaml:"version"` 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 {

View File

@ -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 {

View File

@ -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
} }

View File

@ -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 builder := SqlBuilder{}
params := make([]interface{}, 0)
sql.WriteString(`SELECT * builder.Write(`SELECT
from alert 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 `)
sql.WriteString(`WHERE org_id = ?`) builder.Write(`WHERE alert.org_id = ?`, query.OrgId)
params = append(params, query.OrgId)
if query.DashboardId != 0 { if query.DashboardId != 0 {
sql.WriteString(` AND dashboard_id = ?`) builder.Write(` AND alert.dashboard_id = ?`, query.DashboardId)
params = append(params, query.DashboardId)
} }
if query.PanelId != 0 { if query.PanelId != 0 {
sql.WriteString(` AND panel_id = ?`) builder.Write(` AND alert.panel_id = ?`, query.PanelId)
params = append(params, 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 ?") builder.Write(" LIMIT ?", query.Limit)
params = append(params, query.Limit)
} }
alerts := make([]*m.Alert, 0) alerts := make([]*m.AlertListItemDTO, 0)
if err := x.SQL(sql.String(), params...).Find(&alerts); err != nil { if err := x.SQL(builder.GetSqlString(), builder.params...).Find(&alerts); err != nil {
return err return err
} }

View File

@ -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)

View File

@ -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() return saveDashboard(sess, cmd)
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
}
affectedRows, err = sess.MustCols("folder_id", "has_acl").ID(dash.Id).Update(dash)
}
if err != nil {
return err
}
if affectedRows == 0 {
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,
}
// insert version entry
if affectedRows, err = sess.Insert(dashVersion); err != nil {
return err
} else if affectedRows == 0 {
return m.ErrDashboardNotFound
}
// delete existing tags
_, err = sess.Exec("DELETE FROM dashboard_tag WHERE dashboard_id=?", dash.Id)
if err != nil {
return err
}
// 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) { func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error {
dashWithIdExists := false dash := cmd.GetDashboardModel()
var existingById m.Dashboard
if dash.Id > 0 { if dash.Id > 0 {
dashWithIdExists, err = sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existingById) 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 !dashWithIdExists { if !dashWithIdExists {
return m.ErrDashboardNotFound return m.ErrDashboardNotFound
} }
if dash.Uid == "" { // check for is someone else has written in between
dash.Uid = existingById.Uid 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}
} }
} }
dashWithUidExists := false if dash.Uid == "" {
var existingByUid m.Dashboard uid, err := generateNewDashboardUid(sess, dash.OrgId)
if dash.Uid != "" {
dashWithUidExists, err = sess.Where("org_id=? AND uid=?", dash.OrgId, dash.Uid).Get(&existingByUid)
if err != nil { if err != nil {
return err return err
} }
dash.SetUid(uid)
} }
if !dashWithIdExists && !dashWithUidExists { parentVersion := dash.Version
return nil affectedRows := int64(0)
var err error
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 !cmd.UpdatedAt.IsZero() {
dash.Updated = cmd.UpdatedAt
}
affectedRows, err = sess.MustCols("folder_id").ID(dash.Id).Update(dash)
} }
if dashWithIdExists && dashWithUidExists && existingById.Id != existingByUid.Id { if err != nil {
return m.ErrDashboardWithSameUIDExists return err
} }
existing := existingById if affectedRows == 0 {
return m.ErrDashboardNotFound
if !dashWithIdExists && dashWithUidExists {
dash.Id = existingByUid.Id
existing = existingByUid
} }
if (existing.IsFolder && !cmd.IsFolder) || dashVersion := &m.DashboardVersion{
(!existing.IsFolder && cmd.IsFolder) { DashboardId: dash.Id,
return m.ErrDashboardTypeMismatch 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 // insert version entry
if dash.Version != existing.Version { if affectedRows, err = sess.Insert(dashVersion); err != nil {
if cmd.Overwrite { return err
dash.Version = existing.Version } else if affectedRows == 0 {
} else { return m.ErrDashboardNotFound
return m.ErrDashboardVersionMismatch }
// delete existing tags
_, err = sess.Exec("DELETE FROM dashboard_tag WHERE dashboard_id=?", dash.Id)
if err != nil {
return err
}
// 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 cmd.Result = dash
if existing.PluginId != "" && cmd.Overwrite == false {
return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
}
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.SetId(dashboard.Id)
dashboard.Data.Set("uid", dashboard.Uid) 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
})
}

View File

@ -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 { if _, err := sess.Cols("has_acl").Where("id=?", cmd.DashboardId).Update(&dashboard); err != nil {
return err return err
} }
return nil 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 {
return err
}
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 FROM dashboard as d
LEFT OUTER JOIN ` + dialect.Quote("user") + ` AS u ON u.id = da.user_id LEFT JOIN dashboard folder on folder.id = d.folder_id
LEFT OUTER JOIN team ug on ug.id = da.team_id LEFT JOIN dashboard_acl AS da ON
LEFT OUTER JOIN dashboard d on da.dashboard_id = d.id da.dashboard_id = d.id OR
WHERE dashboard_id ` + dashboardFilter + ` AND da.org_id = ? da.dashboard_id = d.folder_id OR
(
-- Also include default permissions if folder or dashboard field "has_acl" is false -- include default permissions -->
da.org_id = -1 AND (
UNION (folder.id IS NOT NULL AND folder.has_acl = ` + falseStr + `) OR
SELECT (folder.id IS NULL AND d.has_acl = ` + falseStr + `)
da.id, )
da.org_id, )
da.dashboard_id, LEFT JOIN ` + dialect.Quote("user") + ` AS u ON u.id = da.user_id
da.user_id, LEFT JOIN team ug on ug.id = da.team_id
da.team_id, WHERE d.org_id = ? AND d.id = ? AND da.id IS NOT NULL
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
ORDER BY 1 ASC ORDER BY 1 ASC
` `

View File

@ -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{ err := testHelperUpdateDashboardAcl(savedFolder.Id)
OrgId: 1,
AclId: setDashAclCmd.Result.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)
}) })
}) })
}) })

View File

@ -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) testHelperUpdateDashboardAcl(folder.Id)
removeAcl(aclId) testHelperUpdateDashboardAcl(childDash.Id, m.DashboardAcl{DashboardId: folder.Id, OrgId: 1, UserId: otherUser, Permission: m.PERMISSION_EDIT})
updateTestDashboardWithAcl(childDash.Id, otherUser, 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) moveDashboard(1, childDash2.Data, folder1.Id)
So(movedDash.HasAcl, ShouldBeTrue)
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) moveDashboard(1, childDash1.Data, folder2.Id)
So(movedDash.HasAcl, ShouldBeFalse)
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) testHelperUpdateDashboardAcl(childDash1.Id, m.DashboardAcl{DashboardId: childDash1.Id, OrgId: 1, UserId: otherUser, Permission: m.PERMISSION_EDIT})
movedDash := moveDashboard(1, childDash1.Data, folder2.Id) moveDashboard(1, childDash1.Data, folder2.Id)
So(movedDash.HasAcl, ShouldBeTrue)
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[1].Id, ShouldEqual, childDash1.Id)
So(query.Result[2].Id, ShouldEqual, dashInRoot.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)

View File

@ -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
}

View File

@ -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())
})
})
})
}

View File

@ -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,
}
}

View File

@ -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,

View File

@ -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,

View File

@ -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

View File

@ -3,34 +3,11 @@ package sqlstore
import ( import (
"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"
) )
func InitTestDB(t *testing.T) *xorm.Engine {
x, err := xorm.NewEngine(sqlutil.TestDB_Sqlite3.DriverName, sqlutil.TestDB_Sqlite3.ConnStr)
//x, err := xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr)
//x, err := xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, sqlutil.TestDB_Postgres.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

View File

@ -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("?") sql := "SELECT max(id) as id FROM login_attempt WHERE created < ?"
result, err := sess.Query(sql, cmd.OlderThan) 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 {

View File

@ -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)
}

View File

@ -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",
})
} }

View File

@ -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",
})
} }

View File

@ -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() has, err = x.SQL(sql).Get(&r)
// So(err, ShouldBeNil) So(err, ShouldBeNil)
// So(has, ShouldBeTrue)
// fmt.Printf("\nDB Schema after migration: table count: %v\n", len(tables)) 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)
// for _, table := range tables {
// fmt.Printf("\nTable: %v \n", table.Name) mg = NewMigrator(x)
// for _, column := range table.Columns() { AddMigrations(mg)
// fmt.Printf("\t %v \n", column.String(x.Dialect()))
// } err = mg.Start()
// So(err, ShouldBeNil)
// if len(table.Indexes) > 0 {
// fmt.Printf("\n\tIndexes:\n") has, err = x.SQL(sql).Get(&r)
// for _, index := range table.Indexes { So(err, ShouldBeNil)
// fmt.Printf("\t %v (%v) %v \n", index.Name, strings.Join(index.Cols, ","), indexTypes[index.Type]) So(has, ShouldBeTrue)
// } So(r.Count, ShouldEqual, expectedMigrations)
// }
// }
}) })
} }
} }

View File

@ -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)

View File

@ -123,6 +123,31 @@ func TestAccountDataAccess(t *testing.T) {
So(query.Result[0].Role, ShouldEqual, "Admin") So(query.Result[0].Role, ShouldEqual, "Admin")
}) })
Convey("Can get organization users with query", func() {
query := m.GetOrgUsersQuery{
OrgId: ac1.OrgId,
Query: "ac1",
}
err := GetOrgUsers(&query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 1)
So(query.Result[0].Email, ShouldEqual, ac1.Email)
})
Convey("Can get organization users with query and limit", func() {
query := m.GetOrgUsersQuery{
OrgId: ac1.OrgId,
Query: "ac",
Limit: 1,
}
err := GetOrgUsers(&query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 1)
So(query.Result[0].Email, ShouldEqual, ac1.Email)
})
Convey("Can set using org", func() { Convey("Can set using org", func() {
cmd := m.SetUsingOrgCommand{UserId: ac2.Id, OrgId: ac1.Id} cmd := m.SetUsingOrgCommand{UserId: ac2.Id, OrgId: ac1.Id}
err := SetUsingOrg(&cmd) err := SetUsingOrg(&cmd)
@ -174,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() {
@ -209,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)
}

View File

@ -2,6 +2,7 @@ package sqlstore
import ( import (
"fmt" "fmt"
"strings"
"time" "time"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
@ -69,9 +70,30 @@ func UpdateOrgUser(cmd *m.UpdateOrgUserCommand) error {
func GetOrgUsers(query *m.GetOrgUsersQuery) error { func GetOrgUsers(query *m.GetOrgUsersQuery) error {
query.Result = make([]*m.OrgUserDTO, 0) query.Result = make([]*m.OrgUserDTO, 0)
sess := x.Table("org_user") sess := x.Table("org_user")
sess.Join("INNER", "user", fmt.Sprintf("org_user.user_id=%s.id", x.Dialect().Quote("user"))) sess.Join("INNER", "user", fmt.Sprintf("org_user.user_id=%s.id", x.Dialect().Quote("user")))
sess.Where("org_user.org_id=?", query.OrgId)
whereConditions := make([]string, 0)
whereParams := make([]interface{}, 0)
whereConditions = append(whereConditions, "org_user.org_id = ?")
whereParams = append(whereParams, query.OrgId)
if query.Query != "" {
queryWithWildcards := "%" + query.Query + "%"
whereConditions = append(whereConditions, "(email "+dialect.LikeStr()+" ? OR name "+dialect.LikeStr()+" ? OR login "+dialect.LikeStr()+" ?)")
whereParams = append(whereParams, queryWithWildcards, queryWithWildcards, queryWithWildcards)
}
if len(whereConditions) > 0 {
sess.Where(strings.Join(whereConditions, " AND "), whereParams...)
}
if query.Limit > 0 {
sess.Limit(query.Limit, 0)
}
sess.Cols("org_user.org_id", "org_user.user_id", "user.email", "user.login", "org_user.role", "user.last_seen_at") sess.Cols("org_user.org_id", "org_user.user_id", "user.email", "user.login", "org_user.role", "user.last_seen_at")
sess.Asc("user.email", "user.login") sess.Asc("user.email", "user.login")

View File

@ -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{

View File

@ -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 permission m.PermissionType
params []interface{}
} }
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(` sb.sql.WriteString(` LIMIT ?) as ids INNER JOIN dashboard on ids.id = dashboard.id `)
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 { sb.writeDashboardPermissionFilter(sb.signedInUser, sb.permission)
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)
}
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() + " ?")

View File

@ -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()

View File

@ -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...)
}

View File

@ -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
}

View File

@ -11,8 +11,8 @@ type TestDB struct {
ConnStr string ConnStr string
} }
var TestDB_Sqlite3 = TestDB{DriverName: "sqlite3", ConnStr: ":memory:?_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&loc=Local"} 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) {

Some files were not shown because too many files have changed in this diff Show More