+
The image above shows you the top header for a Dashboard.
1. Side menubar toggle: This toggles the side menu, allowing you to focus on the data presented in the dashboard. The side menu provides access to features unrelated to a Dashboard such as Users, Organizations, and Data Sources.
-2. Dashboard dropdown: This dropdown shows you which Dashboard you are currently viewing, and allows you to easily switch to a new Dashboard. From here you can also create a new Dashboard, Import existing Dashboards, and manage Dashboard playlists.
-3. Star Dashboard: Star (or unstar) the current Dashboard. Starred Dashboards will show up on your own Home Dashboard by default, and are a convenient way to mark Dashboards that you're interested in.
-4. Share Dashboard: Share the current dashboard by creating a link or create a static Snapshot of it. Make sure the Dashboard is saved before sharing.
-5. Save dashboard: The current Dashboard will be saved with the current Dashboard name.
-6. Settings: Manage Dashboard settings and features such as Templating and Annotations.
+2. Dashboard dropdown: This dropdown shows you which Dashboard you are currently viewing, and allows you to easily switch to a new Dashboard. From here you can also create a new Dashboard or folder, Import existing Dashboards, and manage Dashboard playlists.
+3. Add Panel: Adds a new panel to the current Dashboard
+4. Star Dashboard: Star (or unstar) the current Dashboard. Starred Dashboards will show up on your own Home Dashboard by default, and are a convenient way to mark Dashboards that you're interested in.
+5. Share Dashboard: Share the current dashboard by creating a link or create a static Snapshot of it. Make sure the Dashboard is saved before sharing.
+6. Save dashboard: The current Dashboard will be saved with the current Dashboard name.
+7. Settings: Manage Dashboard settings and features such as Templating and Annotations.
-## Dashboards, Panels, Rows, the building blocks of Grafana...
+## Dashboards, Panels, the building blocks of Grafana...
-Dashboards are at the core of what Grafana is all about. Dashboards are composed of individual Panels arranged on a number of Rows. Grafana ships with a variety of Panels. Grafana makes it easy to construct the right queries, and customize the display properties so that you can create the perfect Dashboard for your need. Each Panel can interact with data from any configured Grafana Data Source (currently InfluxDB, Graphite, OpenTSDB, Prometheus and Cloudwatch). The [Basic Concepts](/guides/basic_concepts) guide explores these key ideas in detail.
+Dashboards are at the core of what Grafana is all about. Dashboards are composed of individual Panels arranged on a grid. Grafana ships with a variety of Panels. Grafana makes it easy to construct the right queries, and customize the display properties so that you can create the perfect Dashboard for your need. Each Panel can interact with data from any configured Grafana Data Source (currently InfluxDB, Graphite, OpenTSDB, Prometheus and Cloudwatch). The [Basic Concepts](/guides/basic_concepts) guide explores these key ideas in detail.
-
+
1. Zoom out time range
2. Time picker dropdown. Here you can access relative time range options, auto refresh options and set custom absolute time ranges.
3. Manual refresh button. Will cause all panels to refresh (fetch new data).
-4. Row controls menu. Via this menu you can add panels to the row, set row height and more.
-5. Dashboard panel. You edit panels by clicking the panel title.
-6. Graph legend. You can change series colors, y-axis and series visibility directly from the legend.
+4. Dashboard panel. You edit panels by clicking the panel title.
+5. Graph legend. You can change series colors, y-axis and series visibility directly from the legend.
## Adding & Editing Graphs and Panels

-1. You add panels via row menu. The row menu is the icon to the left of each row.
+1. You add panels by clicking the Add panel icon on the top menu.
2. To edit the graph you click on the graph title to open the panel menu, then `Edit`.
3. This should take you to the `Metrics` tab. In this tab you should see the editor for your default data source.
@@ -64,7 +64,7 @@ When you click the `Metrics` tab, you are presented with a Query Editor that is
## Drag-and-Drop panels
-You can Drag-and-Drop Panels within and between Rows. Click and hold the Panel title, and drag it to its new location. You can also easily resize panels by clicking the (-) and (+) icons.
+You can Drag-and-Drop Panels by simply clicking and holding the Panel title, and drag it to its new location. You can also easily resize panels by clicking the (-) and (+) icons.

diff --git a/docs/sources/guides/whats-new-in-v2-1.md b/docs/sources/guides/whats-new-in-v2-1.md
index 68da4f60226..2ad0e3356f0 100644
--- a/docs/sources/guides/whats-new-in-v2-1.md
+++ b/docs/sources/guides/whats-new-in-v2-1.md
@@ -3,11 +3,6 @@ title = "What's New in Grafana v2.1"
description = "Feature & improvement highlights for Grafana v2.1"
keywords = ["grafana", "new", "documentation", "2.1"]
type = "docs"
-[menu.docs]
-name = "Version 2.1"
-identifier = "v2.1"
-parent = "whatsnew"
-weight = 10
+++
# What's new in Grafana v2.1
diff --git a/docs/sources/guides/whats-new-in-v2-5.md b/docs/sources/guides/whats-new-in-v2-5.md
index ff80ec1f4f4..90270ea1121 100644
--- a/docs/sources/guides/whats-new-in-v2-5.md
+++ b/docs/sources/guides/whats-new-in-v2-5.md
@@ -3,11 +3,6 @@ title = "What's New in Grafana v2.5"
description = "Feature & improvement highlights for Grafana v2.5"
keywords = ["grafana", "new", "documentation", "2.5"]
type = "docs"
-[menu.docs]
-name = "Version 2.5"
-identifier = "v2.5"
-parent = "whatsnew"
-weight = 9
+++
# What's new in Grafana v2.5
diff --git a/docs/sources/guides/whats-new-in-v2-6.md b/docs/sources/guides/whats-new-in-v2-6.md
index 0b1e6688e60..b8996680ce6 100644
--- a/docs/sources/guides/whats-new-in-v2-6.md
+++ b/docs/sources/guides/whats-new-in-v2-6.md
@@ -3,11 +3,6 @@ title = "What's New in Grafana v2.6"
description = "Feature & improvement highlights for Grafana v2.6"
keywords = ["grafana", "new", "documentation", "2.6"]
type = "docs"
-[menu.docs]
-name = "Version 2.6"
-identifier = "v2.6"
-parent = "whatsnew"
-weight = 7
+++
# What's new in Grafana v2.6
diff --git a/docs/sources/guides/whats-new-in-v2.md b/docs/sources/guides/whats-new-in-v2.md
index bd92128a12e..499849c8d83 100644
--- a/docs/sources/guides/whats-new-in-v2.md
+++ b/docs/sources/guides/whats-new-in-v2.md
@@ -3,11 +3,6 @@ title = "What's New in Grafana v2.0"
description = "Feature & improvement highlights for Grafana v2.0"
keywords = ["grafana", "new", "documentation", "2.0"]
type = "docs"
-[menu.docs]
-name = "Version 2.0"
-identifier = "v2.0"
-parent = "whatsnew"
-weight = 11
+++
# What's New in Grafana v2.0
diff --git a/docs/sources/http_api/admin.md b/docs/sources/http_api/admin.md
index 716246102bc..0194c69caac 100644
--- a/docs/sources/http_api/admin.md
+++ b/docs/sources/http_api/admin.md
@@ -61,7 +61,7 @@ Content-Type: application/json
"client_id":"some_id",
"client_secret":"************",
"enabled":"false",
- "scopes":"user:email",
+ "scopes":"user:email,read:org",
"team_ids":"",
"token_url":"https://github.com/login/oauth/access_token"
},
diff --git a/docs/sources/http_api/dashboard.md b/docs/sources/http_api/dashboard.md
index 6ddb2360e03..a6ed0c81f4b 100644
--- a/docs/sources/http_api/dashboard.md
+++ b/docs/sources/http_api/dashboard.md
@@ -11,6 +11,17 @@ parent = "http_api"
# Dashboard API
+## Identifier (id) vs unique identifier (uid)
+
+The identifier (id) of a dashboard is an auto-incrementing numeric value and is only unique per Grafana install.
+
+The unique identifier (uid) of a dashboard can be used for uniquely identify a dashboard between multiple Grafana installs.
+It's automatically generated if not provided when creating a dashboard. The uid allows having consistent URL's for accessing
+dashboards and when syncing dashboards between multiple Grafana installs, see [dashboard provisioning](/administration/provisioning/#dashboards)
+for more information. This means that changing the title of a dashboard will not break any bookmarked links to that dashboard.
+
+The uid can have a maximum length of 40 characters.
+
## Create / Update dashboard
`POST /api/dashboards/db`
@@ -28,6 +39,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
{
"dashboard": {
"id": null,
+ "uid": null,
"title": "Production Overview",
"tags": [ "templated" ],
"timezone": "browser",
@@ -38,14 +50,18 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
"schemaVersion": 6,
"version": 0
},
+ "folderId": 0,
"overwrite": false
}
```
JSON Body schema:
-- **dashboard** – The complete dashboard model, id = null to create a new dashboard
-- **overwrite** – Set to true if you want to overwrite existing dashboard with newer version or with same dashboard title.
+- **dashboard** – The complete dashboard model, id = null to create a new dashboard.
+- **dashboard.id** – id = null to create a new dashboard.
+- **dashboard.uid** – Optional [unique identifier](/http_api/dashboard/#identifier-id-vs-unique-identifier-uid) when creating a dashboard. uid = null will generate a new uid.
+- **folderId** – The id of the folder to save the dashboard in.
+- **overwrite** – Set to true if you want to overwrite existing dashboard with newer version, same dashboard title in folder or same dashboard uid.
- **message** - Set a commit message for the version history.
**Example Response**:
@@ -56,9 +72,12 @@ Content-Type: application/json; charset=UTF-8
Content-Length: 78
{
- "slug": "production-overview",
- "status": "success",
- "version": 1
+ "id": 1,
+ "uid": "cIBgcSjkk",
+ "url": "/d/cIBgcSjkk/production-overview",
+ "status": "success",
+ "version": 1,
+ "slug": "production-overview" //deprecated in Grafana v5.0
}
```
@@ -67,10 +86,18 @@ Status Codes:
- **200** – Created
- **400** – Errors (invalid json, missing or invalid fields, etc)
- **401** – Unauthorized
+- **403** – Access denied
- **412** – Precondition failed
-The **412** status code is used when a newer dashboard already exists (newer, its version is greater than the version that was sent). The
-same status code is also used if another dashboard exists with the same title. The response body will look like this:
+The **412** status code is used for explaing that you cannot create the dashboard and why.
+There can be different reasons for this:
+
+- The dashboard has been changed by someone else, `status=version-mismatch`
+- A dashboard with the same name in the folder already exists, `status=name-exists`
+- A dashboard with the same uid already exists, `status=name-exists`
+- The dashboard belongs to plugin `
+{{< docs-imagebox img="/img/docs/v50/export_modal.png" max-width="700px" >}}
### Making a dashboard portable
@@ -31,12 +31,12 @@ the dashboard, and will also be added as an required input when the dashboard is
To import a dashboard open dashboard search and then hit the import button.
-
+{{< docs-imagebox img="/img/docs/v50/import_step1.png" max-width="700px" >}}
From here you can upload a dashboard json file, paste a [Grafana.com](https://grafana.com) dashboard
url or paste dashboard json text directly into the text area.
-
+{{< docs-imagebox img="/img/docs/v50/import_step2.png" max-width="700px" >}}
In step 2 of the import process Grafana will let you change the name of the dashboard, pick what
data source you want the dashboard to use and specify any metric prefixes (if the dashboard use any).
@@ -45,7 +45,7 @@ data source you want the dashboard to use and specify any metric prefixes (if th
Find dashboards for common server applications at [Grafana.com/dashboards](https://grafana.com/dashboards).
-
+{{< docs-imagebox img="/img/docs/v50/gcom_dashboard_list.png" max-width="700px" >}}
## Import & Sharing with Grafana 2.x or 3.0
diff --git a/docs/sources/reference/playlist.md b/docs/sources/reference/playlist.md
index f509ae4dc0d..5a6bf921334 100644
--- a/docs/sources/reference/playlist.md
+++ b/docs/sources/reference/playlist.md
@@ -16,7 +16,7 @@ Since Grafana automatically scales Dashboards to any resolution they're perfect
## Creating a Playlist
-{{< docs-imagebox img="/img/docs/v3/playlist.png" max-width="25rem" class="docs-image--right">}}
+{{< docs-imagebox img="/img/docs/v50/playlist.png" max-width="25rem" class="docs-image--right">}}
The Playlist feature can be accessed from Grafana's sidemenu, in the Dashboard submenu.
diff --git a/docs/sources/reference/search.md b/docs/sources/reference/search.md
index 9fc4d47893c..1bf6fd53e52 100644
--- a/docs/sources/reference/search.md
+++ b/docs/sources/reference/search.md
@@ -10,22 +10,22 @@ weight = 5
# Dashboard Search
-Dashboards can be searched by the dashboard name, filtered by one (or many) tags or filtered by starred status. The dashboard search is accessed through the dashboard picker, available in the dashboard top nav area.
+Dashboards can be searched by the dashboard name, filtered by one (or many) tags or filtered by starred status. The dashboard search is accessed through the dashboard picker, available in the dashboard top nav area. The dashboard search can also be opened by using the shortcut `F`.
-
+
-1. `Dashboard Picker`: The Dashboard Picker is your primary navigation tool to move between dashboards. It is present on all dashboards, and open the Dashboard Search. The dashboard picker also doubles as the title of the current dashboard.
-2. `Search Bar`: The search bar allows you to enter any string and search both database and file based dashboards in real-time.
-3. `Starred`: The starred link allows you to filter the list to display only starred dashboards.
-4. `Tags`: The tags filter allows you to filter the list by dashboard tags.
+1. `Search Bar`: The search bar allows you to enter any string and search both database and file based dashboards in real-time.
+2. `Starred`: Here you find all your starred dashboards.
+3. `Recent`: Here you find the latest created dashboards.
+4. `Folders`: The tags filter allows you to filter the list by dashboard tags.
+5. `Root`: The root contains all dashboards that are not placed in a folder.
+6. `Tags`: The tags filter allows you to filter the list by dashboard tags.
When using only a keyboard, you can use your keyboard arrow keys to navigate the results, hit enter to open the selected dashboard.
## Find by dashboard name
-
-
-To search and load dashboards click the open folder icon in the header or use the shortcut `CTRL`+`F`. Begin typing any part of the desired dashboard names. Search will return results for for any partial string match in real-time, as you type.
+Begin typing any part of the desired dashboard names in the search bar. Search will return results for for any partial string match in real-time, as you type.
Dashboard search is:
- Real-time
@@ -38,21 +38,8 @@ Tags are a great way to organize your dashboards, especially as the number of da
To filter the dashboard list by tag, click on any tag appearing in the right column. The list may be further filtered by clicking on additional tags:
-
-
-Alternately, to see a list of all available tags, click the tags link in the search bar. All tags will be shown, and when a tag is selected, the dashboard search will be instantly filtered:
-
-
+Alternately, to see a list of all available tags, click the tags dropdown menu. All tags will be shown, and when a tag is selected, the dashboard search will be instantly filtered:
When using only a keyboard: `tab` to focus on the *tags* link, `▼` down arrow key to find a tag and select with the `Enter` key.
-**Note**: When multiple tags are selected, Grafana will show dashboards that include **all**.
-
-
-## Filter by Starred
-
-Starring is a great way to organize and find commonly used dashboards. To show only starred dashboards in the list, click the *starred* link in the search bar:
-
-
-
-When using only a keyboard: `tab` to focus on the *stars* link, `▼` down arrow key to find a tag and select with the `Enter` key.
+**Note**: When multiple tags are selected, Grafana will show dashboards that include **all**.
\ No newline at end of file
diff --git a/docs/sources/reference/sharing.md b/docs/sources/reference/sharing.md
index 20aea1acd2e..59c2e0345ea 100644
--- a/docs/sources/reference/sharing.md
+++ b/docs/sources/reference/sharing.md
@@ -24,7 +24,7 @@ A dashboard snapshot is an instant way to share an interactive dashboard publicl
(metric, template and annotation) and panel links, leaving only the visible metric data and series names embedded into your dashboard. Dashboard
snapshots can be accessed by anyone who has the link and can reach the URL.
-
+{{< docs-imagebox img="/img/docs/v50/share_panel_modal.png" max-width="700px" >}}
### Publish snapshots
@@ -70,9 +70,9 @@ Below there should be an interactive Grafana graph embedded in an iframe:
### Export Panel Data
-
+{{< docs-imagebox img="/img/docs/v50/export_panel_data.png" max-width="500px" >}}
-The submenu for a panel can be found by clicking on the title of a panel and then on the hamburger (three horizontal lines) submenu on the left of the context menu.
+The submenu for a panel can be found by clicking on the title of a panel and then on the More submenu.
This menu contains two options for exporting data:
diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md
index 36308adf52f..3a15b4ed7d1 100644
--- a/docs/sources/reference/templating.md
+++ b/docs/sources/reference/templating.md
@@ -1,20 +1,20 @@
+++
-title = "Templating"
+title = "Variables"
keywords = ["grafana", "templating", "documentation", "guide"]
type = "docs"
[menu.docs]
-name = "Templating"
+name = "Variables"
parent = "dashboard_features"
weight = 1
+++
-# Templating
+# Variables
-Templating allows for more interactive and dynamic dashboards. Instead of hard-coding things like server, application
+Variables allows for more interactive and dynamic dashboards. Instead of hard-coding things like server, application
and sensor name in you metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the top of
the dashboard. These dropdowns make it easy to change the data being displayed in your dashboard.
-
+{{< docs-imagebox img="/img/docs/v50/variables_dashboard.png" >}}
## What is a variable?
@@ -43,7 +43,7 @@ is the set of values you can choose from.
## Adding a variable
-
+{{< docs-imagebox img="/img/docs/v50/variables_var_list.png" max-width="800px" >}}
You add variables via Dashboard cogs menu > Templating. This opens up a list of variables and a `New` button to create a new variable.
@@ -133,7 +133,7 @@ Option | Description
*Tags query* | Data source query that should return a list of tags
*Tag values query* | Data source query that should return a list of values for a specified tag key. Use `$tag` in the query to refer the currently selected tag.
-
+{{< docs-imagebox img="/img/docs/v50/variable_dropdown_tags.png" max-width="300px" >}}
### Interval variables
diff --git a/docs/sources/reference/timerange.md b/docs/sources/reference/timerange.md
index a4d6fc62336..4121ed87931 100644
--- a/docs/sources/reference/timerange.md
+++ b/docs/sources/reference/timerange.md
@@ -13,7 +13,7 @@ weight = 7
Grafana provides numerous ways to manage the time ranges of the data being visualized, both at the Dashboard-level and the Panel-level.
-
+
In the top right, you have the master Dashboard time picker (it's in between the 'Zoom out' and the 'Refresh' links).
@@ -39,11 +39,11 @@ Week to date | `now/w` | `now`
Previous Month | `now-1M/M` | `now-1M/M`
-## Dashboard-Level Time Picker Settings
+## Dashboard Time Options
-There are two settings available from the Dashboard Settings area, allowing customization of the auto-refresh intervals and the definition of `now`.
+There are two settings available in the Dashboard Settings General tab, allowing customization of the auto-refresh intervals and the definition of `now`.
-
+
### Auto-Refresh Options
@@ -59,11 +59,11 @@ Users often ask, [when will then be now](https://www.youtube.com/watch?v=VeZ9HhH
You can override the relative time range for individual panels, causing them to be different than what is selected in the Dashboard time picker in the upper right. This allows you to show metrics from different time periods or days at the same time.
-
+{{< docs-imagebox img="/img/docs/v50/panel_time_override.png" max-width="500px" >}}
You control these overrides in panel editor mode and the tab `Time Range`.
-
+{{< docs-imagebox img="/img/docs/v50/time_range_tab.png" max-width="500px" >}}
When you zoom or change the Dashboard time to a custom absolute time range, all panel overrides will be disabled. The panel relative time override is only active when the dashboard time is also relative. The panel timeshift override is always active, even when the dashboard time is absolute.
diff --git a/docs/versions.json b/docs/versions.json
index 03cb40f0e1f..4685360cc29 100644
--- a/docs/versions.json
+++ b/docs/versions.json
@@ -1,6 +1,6 @@
[
- { "version": "v5.0", "path": "/v5.0", "archived": false },
- { "version": "v4.6", "path": "/", "archived": false, "current": true },
+ { "version": "v5.0", "path": "/", "archived": false, "current": true },
+ { "version": "v4.6", "path": "/v4.6", "archived": true },
{ "version": "v4.5", "path": "/v4.5", "archived": true },
{ "version": "v4.4", "path": "/v4.4", "archived": true },
{ "version": "v4.3", "path": "/v4.3", "archived": true },
diff --git a/latest.json b/latest.json
index a746e92c3b3..b476f44a00a 100644
--- a/latest.json
+++ b/latest.json
@@ -1,4 +1,4 @@
{
- "stable": "4.6.2",
- "testing": "4.6.2"
+ "stable": "5.0.0",
+ "testing": "5.0.0"
}
diff --git a/package.json b/package.json
index 3dde98f0f39..df4359ef5c1 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"company": "Grafana Labs"
},
"name": "grafana",
- "version": "5.0.0-beta4",
+ "version": "5.0.1-pre1",
"repository": {
"type": "git",
"url": "http://github.com/grafana/grafana.git"
@@ -155,7 +155,7 @@
"prop-types": "^15.6.0",
"react": "^16.2.0",
"react-dom": "^16.2.0",
- "react-grid-layout": "^0.16.2",
+ "react-grid-layout-grafana": "0.16.0",
"react-highlight-words": "^0.10.0",
"react-popper": "^0.7.5",
"react-select": "^1.1.0",
diff --git a/packaging/publish/publish_testing.sh b/packaging/publish/publish_testing.sh
index 5dea3655251..08ba2a89dd9 100755
--- a/packaging/publish/publish_testing.sh
+++ b/packaging/publish/publish_testing.sh
@@ -1,6 +1,6 @@
#! /usr/bin/env bash
-deb_ver=5.0.0-beta4
-rpm_ver=5.0.0-beta4
+deb_ver=5.0.0-beta5
+rpm_ver=5.0.0-beta5
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_${deb_ver}_amd64.deb
diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go
index 88502804919..53174075c25 100644
--- a/pkg/api/dashboard.go
+++ b/pkg/api/dashboard.go
@@ -411,6 +411,18 @@ func GetDashboardVersion(c *middleware.Context) Response {
// POST /api/dashboards/calculate-diff performs diffs on two dashboards
func CalculateDashboardDiff(c *middleware.Context, apiOptions dtos.CalculateDiffOptions) Response {
+ guardianBase := guardian.New(apiOptions.Base.DashboardId, c.OrgId, c.SignedInUser)
+ if canSave, err := guardianBase.CanSave(); err != nil || !canSave {
+ return dashboardGuardianResponse(err)
+ }
+
+ if apiOptions.Base.DashboardId != apiOptions.New.DashboardId {
+ guardianNew := guardian.New(apiOptions.New.DashboardId, c.OrgId, c.SignedInUser)
+ if canSave, err := guardianNew.CanSave(); err != nil || !canSave {
+ return dashboardGuardianResponse(err)
+ }
+ }
+
options := dashdiffs.Options{
OrgId: c.OrgId,
DiffType: dashdiffs.ParseDiffType(apiOptions.DiffType),
@@ -436,9 +448,9 @@ func CalculateDashboardDiff(c *middleware.Context, apiOptions dtos.CalculateDiff
if options.DiffType == dashdiffs.DiffDelta {
return Respond(200, result.Delta).Header("Content-Type", "application/json")
- } else {
- return Respond(200, result.Delta).Header("Content-Type", "text/html")
}
+
+ return Respond(200, result.Delta).Header("Content-Type", "text/html")
}
// RestoreDashboardVersion restores a dashboard to the given version.
diff --git a/pkg/api/dashboard_permission.go b/pkg/api/dashboard_permission.go
index 351b81885d6..419825644c8 100644
--- a/pkg/api/dashboard_permission.go
+++ b/pkg/api/dashboard_permission.go
@@ -18,13 +18,13 @@ func GetDashboardPermissionList(c *middleware.Context) Response {
return rsp
}
- guardian := guardian.New(dashId, c.OrgId, c.SignedInUser)
+ g := guardian.New(dashId, c.OrgId, c.SignedInUser)
- if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
+ if canAdmin, err := g.CanAdmin(); err != nil || !canAdmin {
return dashboardGuardianResponse(err)
}
- acl, err := guardian.GetAcl()
+ acl, err := g.GetAcl()
if err != nil {
return ApiError(500, "Failed to get dashboard permissions", err)
}
@@ -46,8 +46,8 @@ func UpdateDashboardPermissions(c *middleware.Context, apiCmd dtos.UpdateDashboa
return rsp
}
- guardian := guardian.New(dashId, c.OrgId, c.SignedInUser)
- if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
+ g := guardian.New(dashId, c.OrgId, c.SignedInUser)
+ if canAdmin, err := g.CanAdmin(); err != nil || !canAdmin {
return dashboardGuardianResponse(err)
}
@@ -67,8 +67,13 @@ func UpdateDashboardPermissions(c *middleware.Context, apiCmd dtos.UpdateDashboa
})
}
- if okToUpdate, err := guardian.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, cmd.Items); err != nil || !okToUpdate {
+ if okToUpdate, err := g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, cmd.Items); err != nil || !okToUpdate {
if err != nil {
+ if err == guardian.ErrGuardianPermissionExists ||
+ err == guardian.ErrGuardianOverride {
+ return ApiError(400, err.Error(), err)
+ }
+
return ApiError(500, "Error while checking dashboard permissions", err)
}
diff --git a/pkg/api/dashboard_permission_test.go b/pkg/api/dashboard_permission_test.go
index fd399cfb096..03231338268 100644
--- a/pkg/api/dashboard_permission_test.go
+++ b/pkg/api/dashboard_permission_test.go
@@ -8,183 +8,180 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/services/guardian"
. "github.com/smartystreets/goconvey/convey"
)
func TestDashboardPermissionApiEndpoint(t *testing.T) {
- Convey("Given a dashboard with permissions", t, func() {
- mockResult := []*m.DashboardAclInfoDTO{
- {OrgId: 1, DashboardId: 1, UserId: 2, Permission: m.PERMISSION_VIEW},
- {OrgId: 1, DashboardId: 1, UserId: 3, Permission: m.PERMISSION_EDIT},
- {OrgId: 1, DashboardId: 1, UserId: 4, Permission: m.PERMISSION_ADMIN},
- {OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_VIEW},
- {OrgId: 1, DashboardId: 1, TeamId: 2, Permission: m.PERMISSION_ADMIN},
- }
- dtoRes := transformDashboardAclsToDTOs(mockResult)
-
- 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 {
- query.Result = dtoRes
- return nil
- })
-
- bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
- query.Result = mockResult
- return nil
- })
-
- teamResp := []*m.Team{}
- bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
- query.Result = teamResp
- return nil
- })
-
- // This tests four scenarios:
- // 1. user is an org admin
- // 2. user is an org editor AND has been granted admin permission for the dashboard
- // 3. user is an org viewer AND has been granted edit permission for the dashboard
- // 4. user is an org editor AND has no permissions for the dashboard
-
- Convey("When user is org admin", func() {
- loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:dashboardsId/permissions", m.ROLE_ADMIN, func(sc *scenarioContext) {
- Convey("Should be able to access ACL", func() {
- sc.handlerFunc = GetDashboardPermissionList
- sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
-
- So(sc.resp.Code, ShouldEqual, 200)
-
- respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
- So(err, ShouldBeNil)
- So(len(respJSON.MustArray()), ShouldEqual, 5)
- So(respJSON.GetIndex(0).Get("userId").MustInt(), ShouldEqual, 2)
- So(respJSON.GetIndex(0).Get("permission").MustInt(), ShouldEqual, m.PERMISSION_VIEW)
- })
+ Convey("Dashboard permissions test", t, func() {
+ Convey("Given dashboard not exists", func() {
+ bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
+ return m.ErrDashboardNotFound
})
- loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/permissions", "/api/dashboards/id/:dashboardId/permissions", m.ROLE_ADMIN, func(sc *scenarioContext) {
- getDashboardNotFoundError = m.ErrDashboardNotFound
- sc.handlerFunc = GetDashboardPermissionList
- sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
-
- Convey("Should not be able to access ACL", func() {
- So(sc.resp.Code, ShouldEqual, 404)
- })
+ loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", m.ROLE_EDITOR, func(sc *scenarioContext) {
+ callGetDashboardPermissions(sc)
+ 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},
- },
- }
+ cmd := dtos.UpdateDashboardAclCommand{
+ Items: []dtos.DashboardAclUpdateItem{
+ {UserId: 1000, Permission: m.PERMISSION_ADMIN},
+ },
+ }
- postAclScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:dashboardId/permissions", m.ROLE_ADMIN, cmd, func(sc *scenarioContext) {
- getDashboardNotFoundError = m.ErrDashboardNotFound
- CallPostAcl(sc)
- So(sc.resp.Code, ShouldEqual, 404)
- })
+ updateDashboardPermissionScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", cmd, func(sc *scenarioContext) {
+ callUpdateDashboardPermissions(sc)
+ So(sc.resp.Code, ShouldEqual, 404)
})
})
- Convey("When user is org editor and has admin permission in the ACL", func() {
- loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:dashboardId/permissions", m.ROLE_EDITOR, func(sc *scenarioContext) {
- mockResult = append(mockResult, &m.DashboardAclInfoDTO{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
+ Convey("Given user has no admin permissions", func() {
+ origNewGuardian := guardian.New
+ guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanAdminValue: false})
- Convey("Should be able to access ACL", func() {
- sc.handlerFunc = GetDashboardPermissionList
- sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
-
- So(sc.resp.Code, ShouldEqual, 200)
- })
+ getDashboardQueryResult := m.NewDashboard("Dash")
+ bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
+ query.Result = getDashboardQueryResult
+ return nil
})
- Convey("Should not be able to downgrade their own Admin permission", func() {
- cmd := dtos.UpdateDashboardAclCommand{
- Items: []dtos.DashboardAclUpdateItem{
- {UserId: TestUserID, Permission: m.PERMISSION_EDIT},
- },
- }
-
- postAclScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:dashboardId/permissions", m.ROLE_EDITOR, cmd, func(sc *scenarioContext) {
- mockResult = append(mockResult, &m.DashboardAclInfoDTO{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
-
- CallPostAcl(sc)
- So(sc.resp.Code, ShouldEqual, 403)
- })
+ loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", m.ROLE_EDITOR, func(sc *scenarioContext) {
+ callGetDashboardPermissions(sc)
+ So(sc.resp.Code, ShouldEqual, 403)
})
- Convey("Should be able to update permissions", func() {
- cmd := dtos.UpdateDashboardAclCommand{
- Items: []dtos.DashboardAclUpdateItem{
- {UserId: TestUserID, Permission: m.PERMISSION_ADMIN},
- {UserId: 2, Permission: m.PERMISSION_EDIT},
- },
- }
+ cmd := dtos.UpdateDashboardAclCommand{
+ Items: []dtos.DashboardAclUpdateItem{
+ {UserId: 1000, Permission: m.PERMISSION_ADMIN},
+ },
+ }
- postAclScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:dashboardId/permissions", m.ROLE_EDITOR, cmd, func(sc *scenarioContext) {
- mockResult = append(mockResult, &m.DashboardAclInfoDTO{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
-
- CallPostAcl(sc)
- So(sc.resp.Code, ShouldEqual, 200)
- })
+ updateDashboardPermissionScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", cmd, func(sc *scenarioContext) {
+ callUpdateDashboardPermissions(sc)
+ So(sc.resp.Code, ShouldEqual, 403)
})
- })
-
- Convey("When user is org viewer and has edit permission in the ACL", func() {
- loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:dashboardId/permissions", m.ROLE_VIEWER, func(sc *scenarioContext) {
- mockResult = append(mockResult, &m.DashboardAclInfoDTO{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_EDIT})
-
- // Getting the permissions is an Admin permission
- Convey("Should not be able to get list of permissions from ACL", func() {
- sc.handlerFunc = GetDashboardPermissionList
- sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
-
- So(sc.resp.Code, ShouldEqual, 403)
- })
+ Reset(func() {
+ guardian.New = origNewGuardian
})
})
- Convey("When user is org editor and not in the ACL", func() {
- loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:dashboardsId/permissions", m.ROLE_EDITOR, func(sc *scenarioContext) {
+ Convey("Given user has admin permissions and permissions to update", func() {
+ origNewGuardian := guardian.New
+ guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
+ CanAdminValue: true,
+ CheckPermissionBeforeUpdateValue: true,
+ GetAclValue: []*m.DashboardAclInfoDTO{
+ {OrgId: 1, DashboardId: 1, UserId: 2, Permission: m.PERMISSION_VIEW},
+ {OrgId: 1, DashboardId: 1, UserId: 3, Permission: m.PERMISSION_EDIT},
+ {OrgId: 1, DashboardId: 1, UserId: 4, Permission: m.PERMISSION_ADMIN},
+ {OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_VIEW},
+ {OrgId: 1, DashboardId: 1, TeamId: 2, Permission: m.PERMISSION_ADMIN},
+ },
+ })
- Convey("Should not be able to access ACL", func() {
- sc.handlerFunc = GetDashboardPermissionList
- sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
+ getDashboardQueryResult := m.NewDashboard("Dash")
+ bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
+ query.Result = getDashboardQueryResult
+ return nil
+ })
- So(sc.resp.Code, ShouldEqual, 403)
- })
+ loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", m.ROLE_ADMIN, func(sc *scenarioContext) {
+ callGetDashboardPermissions(sc)
+ So(sc.resp.Code, ShouldEqual, 200)
+ respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
+ So(err, ShouldBeNil)
+ So(len(respJSON.MustArray()), ShouldEqual, 5)
+ So(respJSON.GetIndex(0).Get("userId").MustInt(), ShouldEqual, 2)
+ So(respJSON.GetIndex(0).Get("permission").MustInt(), ShouldEqual, m.PERMISSION_VIEW)
+ })
+
+ cmd := dtos.UpdateDashboardAclCommand{
+ Items: []dtos.DashboardAclUpdateItem{
+ {UserId: 1000, Permission: m.PERMISSION_ADMIN},
+ },
+ }
+
+ updateDashboardPermissionScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", cmd, func(sc *scenarioContext) {
+ callUpdateDashboardPermissions(sc)
+ So(sc.resp.Code, ShouldEqual, 200)
+ })
+
+ Reset(func() {
+ guardian.New = origNewGuardian
+ })
+ })
+
+ Convey("When trying to update permissions with duplicate permissions", func() {
+ origNewGuardian := guardian.New
+ guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
+ CanAdminValue: true,
+ CheckPermissionBeforeUpdateValue: false,
+ CheckPermissionBeforeUpdateError: guardian.ErrGuardianPermissionExists,
+ })
+
+ getDashboardQueryResult := m.NewDashboard("Dash")
+ bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
+ query.Result = getDashboardQueryResult
+ return nil
+ })
+
+ cmd := dtos.UpdateDashboardAclCommand{
+ Items: []dtos.DashboardAclUpdateItem{
+ {UserId: 1000, Permission: m.PERMISSION_ADMIN},
+ },
+ }
+
+ updateDashboardPermissionScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", cmd, func(sc *scenarioContext) {
+ callUpdateDashboardPermissions(sc)
+ So(sc.resp.Code, ShouldEqual, 400)
+ })
+
+ Reset(func() {
+ guardian.New = origNewGuardian
+ })
+ })
+
+ Convey("When trying to override inherited permissions with lower presedence", func() {
+ origNewGuardian := guardian.New
+ guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
+ CanAdminValue: true,
+ CheckPermissionBeforeUpdateValue: false,
+ CheckPermissionBeforeUpdateError: guardian.ErrGuardianOverride},
+ )
+
+ getDashboardQueryResult := m.NewDashboard("Dash")
+ bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
+ query.Result = getDashboardQueryResult
+ return nil
+ })
+
+ cmd := dtos.UpdateDashboardAclCommand{
+ Items: []dtos.DashboardAclUpdateItem{
+ {UserId: 1000, Permission: m.PERMISSION_ADMIN},
+ },
+ }
+
+ updateDashboardPermissionScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", cmd, func(sc *scenarioContext) {
+ callUpdateDashboardPermissions(sc)
+ So(sc.resp.Code, ShouldEqual, 400)
+ })
+
+ Reset(func() {
+ guardian.New = origNewGuardian
})
})
})
}
-func transformDashboardAclsToDTOs(acls []*m.DashboardAclInfoDTO) []*m.DashboardAclInfoDTO {
- dtos := make([]*m.DashboardAclInfoDTO, 0)
-
- for _, acl := range acls {
- dto := &m.DashboardAclInfoDTO{
- OrgId: acl.OrgId,
- DashboardId: acl.DashboardId,
- Permission: acl.Permission,
- UserId: acl.UserId,
- TeamId: acl.TeamId,
- }
- dtos = append(dtos, dto)
- }
-
- return dtos
+func callGetDashboardPermissions(sc *scenarioContext) {
+ sc.handlerFunc = GetDashboardPermissionList
+ sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
}
-func CallPostAcl(sc *scenarioContext) {
+func callUpdateDashboardPermissions(sc *scenarioContext) {
bus.AddHandler("test", func(cmd *m.UpdateDashboardAclCommand) error {
return nil
})
@@ -192,7 +189,7 @@ func CallPostAcl(sc *scenarioContext) {
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
}
-func postAclScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.UpdateDashboardAclCommand, fn scenarioFunc) {
+func updateDashboardPermissionScenario(desc string, url string, routePattern string, cmd dtos.UpdateDashboardAclCommand, fn scenarioFunc) {
Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers()
@@ -200,9 +197,8 @@ func postAclScenario(desc string, url string, routePattern string, role m.RoleTy
sc.defaultHandler = wrap(func(c *middleware.Context) Response {
sc.context = c
- sc.context.UserId = TestUserID
sc.context.OrgId = TestOrgID
- sc.context.OrgRole = role
+ sc.context.UserId = TestUserID
return UpdateDashboardPermissions(c, cmd)
})
diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go
index 734df3da00d..3857fa0b9e1 100644
--- a/pkg/api/dashboard_test.go
+++ b/pkg/api/dashboard_test.go
@@ -743,6 +743,53 @@ func TestDashboardApiEndpoint(t *testing.T) {
}
})
})
+
+ Convey("Given two dashboards being compared", t, func() {
+ mockResult := []*m.DashboardAclInfoDTO{}
+ bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
+ query.Result = mockResult
+ return nil
+ })
+
+ bus.AddHandler("test", func(query *m.GetDashboardVersionQuery) error {
+ query.Result = &m.DashboardVersion{
+ Data: simplejson.NewFromAny(map[string]interface{}{
+ "title": "Dash" + string(query.DashboardId),
+ }),
+ }
+ return nil
+ })
+
+ cmd := dtos.CalculateDiffOptions{
+ Base: dtos.CalculateDiffTarget{
+ DashboardId: 1,
+ Version: 1,
+ },
+ New: dtos.CalculateDiffTarget{
+ DashboardId: 2,
+ Version: 2,
+ },
+ DiffType: "basic",
+ }
+
+ Convey("when user does not have permission", func() {
+ role := m.ROLE_VIEWER
+
+ postDiffScenario("When calling POST on", "/api/dashboards/calculate-diff", "/api/dashboards/calculate-diff", cmd, role, func(sc *scenarioContext) {
+ CallPostDashboard(sc)
+ So(sc.resp.Code, ShouldEqual, 403)
+ })
+ })
+
+ Convey("when user does have permission", func() {
+ role := m.ROLE_ADMIN
+
+ postDiffScenario("When calling POST on", "/api/dashboards/calculate-diff", "/api/dashboards/calculate-diff", cmd, role, func(sc *scenarioContext) {
+ CallPostDashboard(sc)
+ So(sc.resp.Code, ShouldEqual, 200)
+ })
+ })
+ })
}
func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta {
@@ -835,6 +882,28 @@ func postDashboardScenario(desc string, url string, routePattern string, mock *d
})
}
+func postDiffScenario(desc string, url string, routePattern string, cmd dtos.CalculateDiffOptions, role m.RoleType, fn scenarioFunc) {
+ Convey(desc+" "+url, func() {
+ defer bus.ClearBusHandlers()
+
+ sc := setupScenarioContext(url)
+ sc.defaultHandler = wrap(func(c *middleware.Context) Response {
+ sc.context = c
+ sc.context.SignedInUser = &m.SignedInUser{
+ OrgId: TestOrgID,
+ UserId: TestUserID,
+ }
+ sc.context.OrgRole = role
+
+ return CalculateDashboardDiff(c, cmd)
+ })
+
+ sc.m.Post(routePattern, sc.defaultHandler)
+
+ fn(sc)
+ })
+}
+
func (sc *scenarioContext) ToJson() *simplejson.Json {
var result *simplejson.Json
err := json.NewDecoder(sc.resp.Body).Decode(&result)
diff --git a/pkg/api/folder_permission.go b/pkg/api/folder_permission.go
index 7453552d092..7c8aba87337 100644
--- a/pkg/api/folder_permission.go
+++ b/pkg/api/folder_permission.go
@@ -19,13 +19,13 @@ func GetFolderPermissionList(c *middleware.Context) Response {
return toFolderError(err)
}
- guardian := guardian.New(folder.Id, c.OrgId, c.SignedInUser)
+ g := guardian.New(folder.Id, c.OrgId, c.SignedInUser)
- if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
+ if canAdmin, err := g.CanAdmin(); err != nil || !canAdmin {
return toFolderError(m.ErrFolderAccessDenied)
}
- acl, err := guardian.GetAcl()
+ acl, err := g.GetAcl()
if err != nil {
return ApiError(500, "Failed to get folder permissions", err)
}
@@ -50,8 +50,8 @@ func UpdateFolderPermissions(c *middleware.Context, apiCmd dtos.UpdateDashboardA
return toFolderError(err)
}
- guardian := guardian.New(folder.Id, c.OrgId, c.SignedInUser)
- canAdmin, err := guardian.CanAdmin()
+ g := guardian.New(folder.Id, c.OrgId, c.SignedInUser)
+ canAdmin, err := g.CanAdmin()
if err != nil {
return toFolderError(err)
}
@@ -76,8 +76,13 @@ func UpdateFolderPermissions(c *middleware.Context, apiCmd dtos.UpdateDashboardA
})
}
- if okToUpdate, err := guardian.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, cmd.Items); err != nil || !okToUpdate {
+ if okToUpdate, err := g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, cmd.Items); err != nil || !okToUpdate {
if err != nil {
+ if err == guardian.ErrGuardianPermissionExists ||
+ err == guardian.ErrGuardianOverride {
+ return ApiError(400, err.Error(), err)
+ }
+
return ApiError(500, "Error while checking folder permissions", err)
}
diff --git a/pkg/api/folder_permission_test.go b/pkg/api/folder_permission_test.go
index bbae5390b80..552577963b4 100644
--- a/pkg/api/folder_permission_test.go
+++ b/pkg/api/folder_permission_test.go
@@ -5,6 +5,7 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
+ "github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
@@ -15,6 +16,35 @@ import (
func TestFolderPermissionApiEndpoint(t *testing.T) {
Convey("Folder permissions test", t, func() {
+ Convey("Given folder not exists", func() {
+ mock := &fakeFolderService{
+ GetFolderByUidError: m.ErrFolderNotFound,
+ }
+
+ origNewFolderService := dashboards.NewFolderService
+ mockFolderService(mock)
+
+ loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", m.ROLE_EDITOR, func(sc *scenarioContext) {
+ callGetFolderPermissions(sc)
+ So(sc.resp.Code, ShouldEqual, 404)
+ })
+
+ cmd := dtos.UpdateDashboardAclCommand{
+ Items: []dtos.DashboardAclUpdateItem{
+ {UserId: 1000, Permission: m.PERMISSION_ADMIN},
+ },
+ }
+
+ updateFolderPermissionScenario("When calling POST on", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", cmd, func(sc *scenarioContext) {
+ callUpdateFolderPermissions(sc)
+ So(sc.resp.Code, ShouldEqual, 404)
+ })
+
+ Reset(func() {
+ dashboards.NewFolderService = origNewFolderService
+ })
+ })
+
Convey("Given user has no admin permissions", func() {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanAdminValue: false})
@@ -54,7 +84,17 @@ func TestFolderPermissionApiEndpoint(t *testing.T) {
Convey("Given user has admin permissions and permissions to update", func() {
origNewGuardian := guardian.New
- guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanAdminValue: true, CheckPermissionBeforeUpdateValue: true})
+ guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
+ CanAdminValue: true,
+ CheckPermissionBeforeUpdateValue: true,
+ GetAclValue: []*m.DashboardAclInfoDTO{
+ {OrgId: 1, DashboardId: 1, UserId: 2, Permission: m.PERMISSION_VIEW},
+ {OrgId: 1, DashboardId: 1, UserId: 3, Permission: m.PERMISSION_EDIT},
+ {OrgId: 1, DashboardId: 1, UserId: 4, Permission: m.PERMISSION_ADMIN},
+ {OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_VIEW},
+ {OrgId: 1, DashboardId: 1, TeamId: 2, Permission: m.PERMISSION_ADMIN},
+ },
+ })
mock := &fakeFolderService{
GetFolderByUidResult: &m.Folder{
@@ -70,6 +110,11 @@ func TestFolderPermissionApiEndpoint(t *testing.T) {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", m.ROLE_ADMIN, func(sc *scenarioContext) {
callGetFolderPermissions(sc)
So(sc.resp.Code, ShouldEqual, 200)
+ respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
+ So(err, ShouldBeNil)
+ So(len(respJSON.MustArray()), ShouldEqual, 5)
+ So(respJSON.GetIndex(0).Get("userId").MustInt(), ShouldEqual, 2)
+ So(respJSON.GetIndex(0).Get("permission").MustInt(), ShouldEqual, m.PERMISSION_VIEW)
})
cmd := dtos.UpdateDashboardAclCommand{
@@ -88,6 +133,78 @@ func TestFolderPermissionApiEndpoint(t *testing.T) {
dashboards.NewFolderService = origNewFolderService
})
})
+
+ Convey("When trying to update permissions with duplicate permissions", func() {
+ origNewGuardian := guardian.New
+ guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
+ CanAdminValue: true,
+ CheckPermissionBeforeUpdateValue: false,
+ CheckPermissionBeforeUpdateError: guardian.ErrGuardianPermissionExists,
+ })
+
+ mock := &fakeFolderService{
+ GetFolderByUidResult: &m.Folder{
+ Id: 1,
+ Uid: "uid",
+ Title: "Folder",
+ },
+ }
+
+ origNewFolderService := dashboards.NewFolderService
+ mockFolderService(mock)
+
+ cmd := dtos.UpdateDashboardAclCommand{
+ Items: []dtos.DashboardAclUpdateItem{
+ {UserId: 1000, Permission: m.PERMISSION_ADMIN},
+ },
+ }
+
+ updateFolderPermissionScenario("When calling POST on", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", cmd, func(sc *scenarioContext) {
+ callUpdateFolderPermissions(sc)
+ So(sc.resp.Code, ShouldEqual, 400)
+ })
+
+ Reset(func() {
+ guardian.New = origNewGuardian
+ dashboards.NewFolderService = origNewFolderService
+ })
+ })
+
+ Convey("When trying to override inherited permissions with lower presedence", func() {
+ origNewGuardian := guardian.New
+ guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
+ CanAdminValue: true,
+ CheckPermissionBeforeUpdateValue: false,
+ CheckPermissionBeforeUpdateError: guardian.ErrGuardianOverride},
+ )
+
+ mock := &fakeFolderService{
+ GetFolderByUidResult: &m.Folder{
+ Id: 1,
+ Uid: "uid",
+ Title: "Folder",
+ },
+ }
+
+ origNewFolderService := dashboards.NewFolderService
+ mockFolderService(mock)
+
+ cmd := dtos.UpdateDashboardAclCommand{
+ Items: []dtos.DashboardAclUpdateItem{
+ {UserId: 1000, Permission: m.PERMISSION_ADMIN},
+ },
+ }
+
+ updateFolderPermissionScenario("When calling POST on", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", cmd, func(sc *scenarioContext) {
+ callUpdateFolderPermissions(sc)
+ So(sc.resp.Code, ShouldEqual, 400)
+ })
+
+ Reset(func() {
+ guardian.New = origNewGuardian
+ dashboards.NewFolderService = origNewFolderService
+ })
+ })
})
}
diff --git a/pkg/models/dashboard_acl.go b/pkg/models/dashboard_acl.go
index 1fbd2b451b9..5b91b2a70b4 100644
--- a/pkg/models/dashboard_acl.go
+++ b/pkg/models/dashboard_acl.go
@@ -68,6 +68,27 @@ type DashboardAclInfoDTO struct {
Url string `json:"url"`
}
+func (dto *DashboardAclInfoDTO) hasSameRoleAs(other *DashboardAclInfoDTO) bool {
+ if dto.Role == nil || other.Role == nil {
+ return false
+ }
+
+ return dto.UserId <= 0 && dto.TeamId <= 0 && dto.UserId == other.UserId && dto.TeamId == other.TeamId && *dto.Role == *other.Role
+}
+
+func (dto *DashboardAclInfoDTO) hasSameUserAs(other *DashboardAclInfoDTO) bool {
+ return dto.UserId > 0 && dto.UserId == other.UserId
+}
+
+func (dto *DashboardAclInfoDTO) hasSameTeamAs(other *DashboardAclInfoDTO) bool {
+ return dto.TeamId > 0 && dto.TeamId == other.TeamId
+}
+
+// IsDuplicateOf returns true if other item has same role, same user or same team
+func (dto *DashboardAclInfoDTO) IsDuplicateOf(other *DashboardAclInfoDTO) bool {
+ return dto.hasSameRoleAs(other) || dto.hasSameUserAs(other) || dto.hasSameTeamAs(other)
+}
+
//
// COMMANDS
//
diff --git a/pkg/services/alerting/notifiers/teams.go b/pkg/services/alerting/notifiers/teams.go
index 605b2742325..851e5a01c75 100644
--- a/pkg/services/alerting/notifiers/teams.go
+++ b/pkg/services/alerting/notifiers/teams.go
@@ -82,6 +82,8 @@ func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error {
message := this.Mention
if evalContext.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok.
message += " " + evalContext.Rule.Message
+ } else {
+ message += " " // summary must not be empty
}
body := map[string]interface{}{
diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go
index 23d43a53f35..811b38cac86 100644
--- a/pkg/services/guardian/guardian.go
+++ b/pkg/services/guardian/guardian.go
@@ -1,12 +1,19 @@
package guardian
import (
+ "errors"
+
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
+var (
+ ErrGuardianPermissionExists = errors.New("Permission already exists")
+ ErrGuardianOverride = errors.New("You can only override a permission to be higher")
+)
+
// DashboardGuardian to be used for guard against operations without access on dashboard and acl
type DashboardGuardian interface {
CanSave() (bool, error)
@@ -119,14 +126,51 @@ func (g *dashboardGuardianImpl) checkAcl(permission m.PermissionType, acl []*m.D
}
func (g *dashboardGuardianImpl) CheckPermissionBeforeUpdate(permission m.PermissionType, updatePermissions []*m.DashboardAcl) (bool, error) {
- if g.user.OrgRole == m.ROLE_ADMIN {
- return true, nil
+ acl := []*m.DashboardAclInfoDTO{}
+ adminRole := m.ROLE_ADMIN
+ everyoneWithAdminRole := &m.DashboardAclInfoDTO{DashboardId: g.dashId, UserId: 0, TeamId: 0, Role: &adminRole, Permission: m.PERMISSION_ADMIN}
+
+ // validate that duplicate permissions don't exists
+ for _, p := range updatePermissions {
+ aclItem := &m.DashboardAclInfoDTO{DashboardId: p.DashboardId, UserId: p.UserId, TeamId: p.TeamId, Role: p.Role, Permission: p.Permission}
+ if aclItem.IsDuplicateOf(everyoneWithAdminRole) {
+ return false, ErrGuardianPermissionExists
+ }
+
+ for _, a := range acl {
+ if a.IsDuplicateOf(aclItem) {
+ return false, ErrGuardianPermissionExists
+ }
+ }
+
+ acl = append(acl, aclItem)
}
- acl := []*m.DashboardAclInfoDTO{}
+ existingPermissions, err := g.GetAcl()
+ if err != nil {
+ return false, err
+ }
- for _, p := range updatePermissions {
- acl = append(acl, &m.DashboardAclInfoDTO{UserId: p.UserId, TeamId: p.TeamId, Role: p.Role, Permission: p.Permission})
+ // validate overridden permissions to be higher
+ for _, a := range acl {
+ for _, existingPerm := range existingPermissions {
+ // handle default permissions
+ if existingPerm.DashboardId == -1 {
+ existingPerm.DashboardId = g.dashId
+ }
+
+ if a.DashboardId == existingPerm.DashboardId {
+ continue
+ }
+
+ if a.IsDuplicateOf(existingPerm) && a.Permission <= existingPerm.Permission {
+ return false, ErrGuardianOverride
+ }
+ }
+ }
+
+ if g.user.OrgRole == m.ROLE_ADMIN {
+ return true, nil
}
return g.checkAcl(permission, acl)
@@ -143,6 +187,13 @@ func (g *dashboardGuardianImpl) GetAcl() ([]*m.DashboardAclInfoDTO, error) {
return nil, err
}
+ for _, a := range query.Result {
+ // handle default permissions
+ if a.DashboardId == -1 {
+ a.DashboardId = g.dashId
+ }
+ }
+
g.acl = query.Result
return g.acl, nil
}
@@ -169,6 +220,8 @@ type FakeDashboardGuardian struct {
CanAdminValue bool
HasPermissionValue bool
CheckPermissionBeforeUpdateValue bool
+ CheckPermissionBeforeUpdateError error
+ GetAclValue []*m.DashboardAclInfoDTO
}
func (g *FakeDashboardGuardian) CanSave() (bool, error) {
@@ -192,11 +245,11 @@ func (g *FakeDashboardGuardian) HasPermission(permission m.PermissionType) (bool
}
func (g *FakeDashboardGuardian) CheckPermissionBeforeUpdate(permission m.PermissionType, updatePermissions []*m.DashboardAcl) (bool, error) {
- return g.CheckPermissionBeforeUpdateValue, nil
+ return g.CheckPermissionBeforeUpdateValue, g.CheckPermissionBeforeUpdateError
}
func (g *FakeDashboardGuardian) GetAcl() ([]*m.DashboardAclInfoDTO, error) {
- return nil, nil
+ return g.GetAclValue, nil
}
func MockDashboardGuardian(mock *FakeDashboardGuardian) {
diff --git a/pkg/services/guardian/guardian_test.go b/pkg/services/guardian/guardian_test.go
new file mode 100644
index 00000000000..bb7e6bd1a72
--- /dev/null
+++ b/pkg/services/guardian/guardian_test.go
@@ -0,0 +1,711 @@
+package guardian
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/grafana/grafana/pkg/bus"
+
+ m "github.com/grafana/grafana/pkg/models"
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+func TestGuardian(t *testing.T) {
+ Convey("Guardian permission tests", t, func() {
+ orgRoleScenario("Given user has admin org role", m.ROLE_ADMIN, func(sc *scenarioContext) {
+ canAdmin, _ := sc.g.CanAdmin()
+ canEdit, _ := sc.g.CanEdit()
+ canSave, _ := sc.g.CanSave()
+ canView, _ := sc.g.CanView()
+ So(canAdmin, ShouldBeTrue)
+ So(canEdit, ShouldBeTrue)
+ So(canSave, ShouldBeTrue)
+ So(canView, ShouldBeTrue)
+
+ Convey("When trying to update permissions", func() {
+ Convey("With duplicate user permissions should return error", func() {
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_VIEW},
+ {OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN},
+ }
+ _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(err, ShouldEqual, ErrGuardianPermissionExists)
+ })
+
+ Convey("With duplicate team permissions should return error", func() {
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_VIEW},
+ {OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_ADMIN},
+ }
+ _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(err, ShouldEqual, ErrGuardianPermissionExists)
+ })
+
+ Convey("With duplicate everyone with editor role permission should return error", func() {
+ r := m.ROLE_EDITOR
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_VIEW},
+ {OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_ADMIN},
+ }
+ _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(err, ShouldEqual, ErrGuardianPermissionExists)
+ })
+
+ Convey("With duplicate everyone with viewer role permission should return error", func() {
+ r := m.ROLE_VIEWER
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_VIEW},
+ {OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_ADMIN},
+ }
+ _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(err, ShouldEqual, ErrGuardianPermissionExists)
+ })
+
+ Convey("With everyone with admin role permission should return error", func() {
+ r := m.ROLE_ADMIN
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_ADMIN},
+ }
+ _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(err, ShouldEqual, ErrGuardianPermissionExists)
+ })
+ })
+
+ Convey("Given default permissions", func() {
+ editor := m.ROLE_EDITOR
+ viewer := m.ROLE_VIEWER
+ existingPermissions := []*m.DashboardAclInfoDTO{
+ {OrgId: 1, DashboardId: -1, Role: &editor, Permission: m.PERMISSION_EDIT},
+ {OrgId: 1, DashboardId: -1, Role: &viewer, Permission: m.PERMISSION_VIEW},
+ }
+
+ bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
+ query.Result = existingPermissions
+ return nil
+ })
+
+ Convey("When trying to update dashboard permissions without everyone with role editor can edit should be allowed", func() {
+ r := m.ROLE_VIEWER
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_VIEW},
+ }
+ ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(ok, ShouldBeTrue)
+ })
+
+ Convey("When trying to update dashboard permissions without everyone with role viewer can view should be allowed", func() {
+ r := m.ROLE_EDITOR
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_EDIT},
+ }
+ ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(ok, ShouldBeTrue)
+ })
+ })
+
+ Convey("Given parent folder has user admin permission", func() {
+ existingPermissions := []*m.DashboardAclInfoDTO{
+ {OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_ADMIN},
+ }
+
+ bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
+ query.Result = existingPermissions
+ return nil
+ })
+
+ Convey("When trying to update dashboard permissions with admin user permission should return error", func() {
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_ADMIN},
+ }
+ _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(err, ShouldEqual, ErrGuardianOverride)
+ })
+
+ Convey("When trying to update dashboard permissions with edit user permission should return error", func() {
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_EDIT},
+ }
+ _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(err, ShouldEqual, ErrGuardianOverride)
+ })
+
+ Convey("When trying to update dashboard permissions with view user permission should return error", func() {
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_VIEW},
+ }
+ _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(err, ShouldEqual, ErrGuardianOverride)
+ })
+ })
+
+ Convey("Given parent folder has user edit permission", func() {
+ existingPermissions := []*m.DashboardAclInfoDTO{
+ {OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_EDIT},
+ }
+
+ bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
+ query.Result = existingPermissions
+ return nil
+ })
+
+ Convey("When trying to update dashboard permissions with admin user permission should be allowed", func() {
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_ADMIN},
+ }
+ ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(ok, ShouldBeTrue)
+ })
+
+ Convey("When trying to update dashboard permissions with edit user permission should return error", func() {
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_EDIT},
+ }
+ _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(err, ShouldEqual, ErrGuardianOverride)
+ })
+
+ Convey("When trying to update dashboard permissions with view user permission should return error", func() {
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_VIEW},
+ }
+ _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(err, ShouldEqual, ErrGuardianOverride)
+ })
+ })
+
+ Convey("Given parent folder has user view permission", func() {
+ existingPermissions := []*m.DashboardAclInfoDTO{
+ {OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW},
+ }
+
+ bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
+ query.Result = existingPermissions
+ return nil
+ })
+
+ Convey("When trying to update dashboard permissions with admin user permission should be allowed", func() {
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_ADMIN},
+ }
+ ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(ok, ShouldBeTrue)
+ })
+
+ Convey("When trying to update dashboard permissions with edit user permission should be allowed", func() {
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_EDIT},
+ }
+ ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(ok, ShouldBeTrue)
+ })
+
+ Convey("When trying to update dashboard permissions with view user permission should return error", func() {
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_VIEW},
+ }
+ _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(err, ShouldEqual, ErrGuardianOverride)
+ })
+ })
+
+ Convey("Given parent folder has team admin permission", func() {
+ existingPermissions := []*m.DashboardAclInfoDTO{
+ {OrgId: 1, DashboardId: 2, TeamId: 1, Permission: m.PERMISSION_ADMIN},
+ }
+
+ bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
+ query.Result = existingPermissions
+ return nil
+ })
+
+ Convey("When trying to update dashboard permissions with admin team permission should return error", func() {
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_ADMIN},
+ }
+ _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(err, ShouldEqual, ErrGuardianOverride)
+ })
+
+ Convey("When trying to update dashboard permissions with edit team permission should return error", func() {
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_EDIT},
+ }
+ _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(err, ShouldEqual, ErrGuardianOverride)
+ })
+
+ Convey("When trying to update dashboard permissions with view team permission should return error", func() {
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_VIEW},
+ }
+ _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(err, ShouldEqual, ErrGuardianOverride)
+ })
+ })
+
+ Convey("Given parent folder has team edit permission", func() {
+ existingPermissions := []*m.DashboardAclInfoDTO{
+ {OrgId: 1, DashboardId: 2, TeamId: 1, Permission: m.PERMISSION_EDIT},
+ }
+
+ bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
+ query.Result = existingPermissions
+ return nil
+ })
+
+ Convey("When trying to update dashboard permissions with admin team permission should be allowed", func() {
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_ADMIN},
+ }
+ ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(ok, ShouldBeTrue)
+ })
+
+ Convey("When trying to update dashboard permissions with edit team permission should return error", func() {
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_EDIT},
+ }
+ _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(err, ShouldEqual, ErrGuardianOverride)
+ })
+
+ Convey("When trying to update dashboard permissions with view team permission should return error", func() {
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_VIEW},
+ }
+ _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(err, ShouldEqual, ErrGuardianOverride)
+ })
+ })
+
+ Convey("Given parent folder has team view permission", func() {
+ existingPermissions := []*m.DashboardAclInfoDTO{
+ {OrgId: 1, DashboardId: 2, TeamId: 1, Permission: m.PERMISSION_VIEW},
+ }
+
+ bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
+ query.Result = existingPermissions
+ return nil
+ })
+
+ Convey("When trying to update dashboard permissions with admin team permission should be allowed", func() {
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_ADMIN},
+ }
+ ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(ok, ShouldBeTrue)
+ })
+
+ Convey("When trying to update dashboard permissions with edit team permission should be allowed", func() {
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_EDIT},
+ }
+ ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(ok, ShouldBeTrue)
+ })
+
+ Convey("When trying to update dashboard permissions with view team permission should return error", func() {
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_VIEW},
+ }
+ _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(err, ShouldEqual, ErrGuardianOverride)
+ })
+ })
+
+ Convey("Given parent folder has editor role with edit permission", func() {
+ r := m.ROLE_EDITOR
+ existingPermissions := []*m.DashboardAclInfoDTO{
+ {OrgId: 1, DashboardId: 2, Role: &r, Permission: m.PERMISSION_EDIT},
+ }
+
+ bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
+ query.Result = existingPermissions
+ return nil
+ })
+
+ Convey("When trying to update dashboard permissions with everyone with editor role can admin permission should be allowed", func() {
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_ADMIN},
+ }
+ ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(ok, ShouldBeTrue)
+ })
+
+ Convey("When trying to update dashboard permissions with everyone with editor role can edit permission should return error", func() {
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_EDIT},
+ }
+ _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(err, ShouldEqual, ErrGuardianOverride)
+ })
+
+ Convey("When trying to update dashboard permissions with everyone with editor role can view permission should return error", func() {
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_VIEW},
+ }
+ _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(err, ShouldEqual, ErrGuardianOverride)
+ })
+ })
+
+ Convey("Given parent folder has editor role with view permission", func() {
+ r := m.ROLE_EDITOR
+ existingPermissions := []*m.DashboardAclInfoDTO{
+ {OrgId: 1, DashboardId: 2, Role: &r, Permission: m.PERMISSION_VIEW},
+ }
+
+ bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
+ query.Result = existingPermissions
+ return nil
+ })
+
+ Convey("When trying to update dashboard permissions with everyone with viewer role can admin permission should be allowed", func() {
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_ADMIN},
+ }
+ ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(ok, ShouldBeTrue)
+ })
+
+ Convey("When trying to update dashboard permissions with everyone with viewer role can edit permission should be allowed", func() {
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_EDIT},
+ }
+ ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(ok, ShouldBeTrue)
+ })
+
+ Convey("When trying to update dashboard permissions with everyone with viewer role can view permission should return error", func() {
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_VIEW},
+ }
+ _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(err, ShouldEqual, ErrGuardianOverride)
+ })
+ })
+ })
+
+ orgRoleScenario("Given user has editor org role", m.ROLE_EDITOR, func(sc *scenarioContext) {
+ everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
+ canAdmin, _ := sc.g.CanAdmin()
+ canEdit, _ := sc.g.CanEdit()
+ canSave, _ := sc.g.CanSave()
+ canView, _ := sc.g.CanView()
+ So(canAdmin, ShouldBeTrue)
+ So(canEdit, ShouldBeTrue)
+ So(canSave, ShouldBeTrue)
+ So(canView, ShouldBeTrue)
+ })
+
+ everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
+ canAdmin, _ := sc.g.CanAdmin()
+ canEdit, _ := sc.g.CanEdit()
+ canSave, _ := sc.g.CanSave()
+ canView, _ := sc.g.CanView()
+ So(canAdmin, ShouldBeFalse)
+ So(canEdit, ShouldBeTrue)
+ So(canSave, ShouldBeTrue)
+ So(canView, ShouldBeTrue)
+ })
+
+ everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
+ canAdmin, _ := sc.g.CanAdmin()
+ canEdit, _ := sc.g.CanEdit()
+ canSave, _ := sc.g.CanSave()
+ canView, _ := sc.g.CanView()
+ So(canAdmin, ShouldBeFalse)
+ So(canEdit, ShouldBeFalse)
+ So(canSave, ShouldBeFalse)
+ So(canView, ShouldBeTrue)
+ })
+
+ everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
+ canAdmin, _ := sc.g.CanAdmin()
+ canEdit, _ := sc.g.CanEdit()
+ canSave, _ := sc.g.CanSave()
+ canView, _ := sc.g.CanView()
+ So(canAdmin, ShouldBeFalse)
+ So(canEdit, ShouldBeFalse)
+ So(canSave, ShouldBeFalse)
+ So(canView, ShouldBeFalse)
+ })
+
+ everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
+ canAdmin, _ := sc.g.CanAdmin()
+ canEdit, _ := sc.g.CanEdit()
+ canSave, _ := sc.g.CanSave()
+ canView, _ := sc.g.CanView()
+ So(canAdmin, ShouldBeFalse)
+ So(canEdit, ShouldBeFalse)
+ So(canSave, ShouldBeFalse)
+ So(canView, ShouldBeFalse)
+ })
+
+ everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
+ canAdmin, _ := sc.g.CanAdmin()
+ canEdit, _ := sc.g.CanEdit()
+ canSave, _ := sc.g.CanSave()
+ canView, _ := sc.g.CanView()
+ So(canAdmin, ShouldBeFalse)
+ So(canEdit, ShouldBeFalse)
+ So(canSave, ShouldBeFalse)
+ So(canView, ShouldBeFalse)
+ })
+
+ userWithPermissionScenario(m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
+ canAdmin, _ := sc.g.CanAdmin()
+ canEdit, _ := sc.g.CanEdit()
+ canSave, _ := sc.g.CanSave()
+ canView, _ := sc.g.CanView()
+ So(canAdmin, ShouldBeTrue)
+ So(canEdit, ShouldBeTrue)
+ So(canSave, ShouldBeTrue)
+ So(canView, ShouldBeTrue)
+ })
+
+ userWithPermissionScenario(m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
+ canAdmin, _ := sc.g.CanAdmin()
+ canEdit, _ := sc.g.CanEdit()
+ canSave, _ := sc.g.CanSave()
+ canView, _ := sc.g.CanView()
+ So(canAdmin, ShouldBeFalse)
+ So(canEdit, ShouldBeTrue)
+ So(canSave, ShouldBeTrue)
+ So(canView, ShouldBeTrue)
+ })
+
+ userWithPermissionScenario(m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
+ canAdmin, _ := sc.g.CanAdmin()
+ canEdit, _ := sc.g.CanEdit()
+ canSave, _ := sc.g.CanSave()
+ canView, _ := sc.g.CanView()
+ So(canAdmin, ShouldBeFalse)
+ So(canEdit, ShouldBeFalse)
+ So(canSave, ShouldBeFalse)
+ So(canView, ShouldBeTrue)
+ })
+
+ teamWithPermissionScenario(m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
+ canAdmin, _ := sc.g.CanAdmin()
+ canEdit, _ := sc.g.CanEdit()
+ canSave, _ := sc.g.CanSave()
+ canView, _ := sc.g.CanView()
+ So(canAdmin, ShouldBeTrue)
+ So(canEdit, ShouldBeTrue)
+ So(canSave, ShouldBeTrue)
+ So(canView, ShouldBeTrue)
+ })
+
+ teamWithPermissionScenario(m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
+ canAdmin, _ := sc.g.CanAdmin()
+ canEdit, _ := sc.g.CanEdit()
+ canSave, _ := sc.g.CanSave()
+ canView, _ := sc.g.CanView()
+ So(canAdmin, ShouldBeFalse)
+ So(canEdit, ShouldBeTrue)
+ So(canSave, ShouldBeTrue)
+ So(canView, ShouldBeTrue)
+ })
+
+ teamWithPermissionScenario(m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
+ canAdmin, _ := sc.g.CanAdmin()
+ canEdit, _ := sc.g.CanEdit()
+ canSave, _ := sc.g.CanSave()
+ canView, _ := sc.g.CanView()
+ So(canAdmin, ShouldBeFalse)
+ So(canEdit, ShouldBeFalse)
+ So(canSave, ShouldBeFalse)
+ So(canView, ShouldBeTrue)
+ })
+
+ Convey("When trying to update permissions should return false", func() {
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_VIEW},
+ {OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN},
+ }
+ ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(ok, ShouldBeFalse)
+ })
+ })
+
+ orgRoleScenario("Given user has viewer org role", m.ROLE_VIEWER, func(sc *scenarioContext) {
+ everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
+ canAdmin, _ := sc.g.CanAdmin()
+ canEdit, _ := sc.g.CanEdit()
+ canSave, _ := sc.g.CanSave()
+ canView, _ := sc.g.CanView()
+ So(canAdmin, ShouldBeFalse)
+ So(canEdit, ShouldBeFalse)
+ So(canSave, ShouldBeFalse)
+ So(canView, ShouldBeFalse)
+ })
+
+ everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
+ canAdmin, _ := sc.g.CanAdmin()
+ canEdit, _ := sc.g.CanEdit()
+ canSave, _ := sc.g.CanSave()
+ canView, _ := sc.g.CanView()
+ So(canAdmin, ShouldBeFalse)
+ So(canEdit, ShouldBeFalse)
+ So(canSave, ShouldBeFalse)
+ So(canView, ShouldBeFalse)
+ })
+
+ everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
+ canAdmin, _ := sc.g.CanAdmin()
+ canEdit, _ := sc.g.CanEdit()
+ canSave, _ := sc.g.CanSave()
+ canView, _ := sc.g.CanView()
+ So(canAdmin, ShouldBeFalse)
+ So(canEdit, ShouldBeFalse)
+ So(canSave, ShouldBeFalse)
+ So(canView, ShouldBeFalse)
+ })
+
+ everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
+ canAdmin, _ := sc.g.CanAdmin()
+ canEdit, _ := sc.g.CanEdit()
+ canSave, _ := sc.g.CanSave()
+ canView, _ := sc.g.CanView()
+ So(canAdmin, ShouldBeTrue)
+ So(canEdit, ShouldBeTrue)
+ So(canSave, ShouldBeTrue)
+ So(canView, ShouldBeTrue)
+ })
+
+ everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
+ canAdmin, _ := sc.g.CanAdmin()
+ canEdit, _ := sc.g.CanEdit()
+ canSave, _ := sc.g.CanSave()
+ canView, _ := sc.g.CanView()
+ So(canAdmin, ShouldBeFalse)
+ So(canEdit, ShouldBeTrue)
+ So(canSave, ShouldBeTrue)
+ So(canView, ShouldBeTrue)
+ })
+
+ everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
+ canAdmin, _ := sc.g.CanAdmin()
+ canEdit, _ := sc.g.CanEdit()
+ canSave, _ := sc.g.CanSave()
+ canView, _ := sc.g.CanView()
+ So(canAdmin, ShouldBeFalse)
+ So(canEdit, ShouldBeFalse)
+ So(canSave, ShouldBeFalse)
+ So(canView, ShouldBeTrue)
+ })
+
+ userWithPermissionScenario(m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
+ canAdmin, _ := sc.g.CanAdmin()
+ canEdit, _ := sc.g.CanEdit()
+ canSave, _ := sc.g.CanSave()
+ canView, _ := sc.g.CanView()
+ So(canAdmin, ShouldBeTrue)
+ So(canEdit, ShouldBeTrue)
+ So(canSave, ShouldBeTrue)
+ So(canView, ShouldBeTrue)
+ })
+
+ userWithPermissionScenario(m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
+ canAdmin, _ := sc.g.CanAdmin()
+ canEdit, _ := sc.g.CanEdit()
+ canSave, _ := sc.g.CanSave()
+ canView, _ := sc.g.CanView()
+ So(canAdmin, ShouldBeFalse)
+ So(canEdit, ShouldBeTrue)
+ So(canSave, ShouldBeTrue)
+ So(canView, ShouldBeTrue)
+ })
+
+ userWithPermissionScenario(m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
+ canAdmin, _ := sc.g.CanAdmin()
+ canEdit, _ := sc.g.CanEdit()
+ canSave, _ := sc.g.CanSave()
+ canView, _ := sc.g.CanView()
+ So(canAdmin, ShouldBeFalse)
+ So(canEdit, ShouldBeFalse)
+ So(canSave, ShouldBeFalse)
+ So(canView, ShouldBeTrue)
+ })
+
+ Convey("When trying to update permissions should return false", func() {
+ p := []*m.DashboardAcl{
+ {OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_VIEW},
+ {OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN},
+ }
+ ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+ So(ok, ShouldBeFalse)
+ })
+ })
+ })
+}
+
+type scenarioContext struct {
+ g DashboardGuardian
+}
+
+type scenarioFunc func(c *scenarioContext)
+
+func orgRoleScenario(desc string, role m.RoleType, fn scenarioFunc) {
+ user := &m.SignedInUser{
+ UserId: 1,
+ OrgId: 1,
+ OrgRole: role,
+ }
+ guard := New(1, 1, user)
+ sc := &scenarioContext{
+ g: guard,
+ }
+
+ Convey(desc, func() {
+ fn(sc)
+ })
+}
+
+func permissionScenario(desc string, sc *scenarioContext, permissions []*m.DashboardAclInfoDTO, fn scenarioFunc) {
+ bus.ClearBusHandlers()
+
+ bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
+ query.Result = permissions
+ return nil
+ })
+
+ teams := []*m.Team{}
+
+ for _, p := range permissions {
+ if p.TeamId > 0 {
+ teams = append(teams, &m.Team{Id: p.TeamId})
+ }
+ }
+
+ bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
+ query.Result = teams
+ return nil
+ })
+
+ Convey(desc, func() {
+ fn(sc)
+ })
+}
+
+func userWithPermissionScenario(permission m.PermissionType, sc *scenarioContext, fn scenarioFunc) {
+ p := []*m.DashboardAclInfoDTO{
+ {OrgId: 1, DashboardId: 1, UserId: 1, Permission: permission},
+ }
+ permissionScenario(fmt.Sprintf("and user has permission to %s item", permission), sc, p, fn)
+}
+
+func teamWithPermissionScenario(permission m.PermissionType, sc *scenarioContext, fn scenarioFunc) {
+ p := []*m.DashboardAclInfoDTO{
+ {OrgId: 1, DashboardId: 1, TeamId: 1, Permission: permission},
+ }
+ permissionScenario(fmt.Sprintf("and team has permission to %s item", permission), sc, p, fn)
+}
+
+func everyoneWithRoleScenario(role m.RoleType, permission m.PermissionType, sc *scenarioContext, fn scenarioFunc) {
+ p := []*m.DashboardAclInfoDTO{
+ {OrgId: 1, DashboardId: 1, UserId: -1, Role: &role, Permission: permission},
+ }
+ permissionScenario(fmt.Sprintf("and everyone with %s role can %s item", role, permission), sc, p, fn)
+}
diff --git a/pkg/services/provisioning/dashboards/test-configs/broken-configs/commented.yaml b/pkg/services/provisioning/dashboards/test-configs/broken-configs/commented.yaml
index e40612af508..f0dcca9b47a 100644
--- a/pkg/services/provisioning/dashboards/test-configs/broken-configs/commented.yaml
+++ b/pkg/services/provisioning/dashboards/test-configs/broken-configs/commented.yaml
@@ -3,4 +3,4 @@
# folder: ''
# type: file
# options:
-# folder: /var/lib/grafana/dashboards
+# path: /var/lib/grafana/dashboards
diff --git a/pkg/social/github_oauth.go b/pkg/social/github_oauth.go
index 6f00cd2cd9e..815c684cf03 100644
--- a/pkg/social/github_oauth.go
+++ b/pkg/social/github_oauth.go
@@ -195,10 +195,9 @@ func (s *SocialGithub) FetchOrganizations(client *http.Client, organizationsUrl
func (s *SocialGithub) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
var data struct {
- Id int `json:"id"`
- Login string `json:"login"`
- Email string `json:"email"`
- OrganizationsUrl string `json:"organizations_url"`
+ Id int `json:"id"`
+ Login string `json:"login"`
+ Email string `json:"email"`
}
response, err := HttpGet(client, s.apiUrl)
@@ -217,11 +216,13 @@ func (s *SocialGithub) UserInfo(client *http.Client, token *oauth2.Token) (*Basi
Email: data.Email,
}
+ organizationsUrl := fmt.Sprintf(s.apiUrl + "/orgs")
+
if !s.IsTeamMember(client) {
return nil, ErrMissingTeamMembership
}
- if !s.IsOrganizationMember(client, data.OrganizationsUrl) {
+ if !s.IsOrganizationMember(client, organizationsUrl) {
return nil, ErrMissingOrganizationMembership
}
diff --git a/public/app/core/components/Permissions/AddPermissions.jest.tsx b/public/app/core/components/Permissions/AddPermissions.jest.tsx
index 9c01eee70b1..fe97c4c7e62 100644
--- a/public/app/core/components/Permissions/AddPermissions.jest.tsx
+++ b/public/app/core/components/Permissions/AddPermissions.jest.tsx
@@ -17,7 +17,7 @@ describe('AddPermissions', () => {
])
);
- backendSrv.post = jest.fn();
+ backendSrv.post = jest.fn(() => Promise.resolve({}));
store = RootStore.create(
{},
diff --git a/public/app/core/components/Permissions/AddPermissions.tsx b/public/app/core/components/Permissions/AddPermissions.tsx
index 94afa7c1180..07ccfdbbef5 100644
--- a/public/app/core/components/Permissions/AddPermissions.tsx
+++ b/public/app/core/components/Permissions/AddPermissions.tsx
@@ -135,14 +135,6 @@ class AddPermissions extends Component