From 047d023022acb074a071b3e964b257612d3edd4c Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 14 Dec 2017 15:13:26 +0100 Subject: [PATCH 01/58] docs: first draft of dashboard folders docs --- docs/sources/reference/dashboard_folders.md | 97 +++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 docs/sources/reference/dashboard_folders.md diff --git a/docs/sources/reference/dashboard_folders.md b/docs/sources/reference/dashboard_folders.md new file mode 100644 index 00000000000..0ba6d49ed9a --- /dev/null +++ b/docs/sources/reference/dashboard_folders.md @@ -0,0 +1,97 @@ ++++ +title = "Dashboard Folders" +keywords = ["grafana", "dashboard", "dashboard folders", "folder", "folders", "documentation", "guide"] +type = "docs" +[menu.docs] +name = "Folders" +parent = "dashboard_features" +weight = 3 ++++ + +# Dashboard Folders + +Folders are a way to organize and group dashboards - very useful if you have a lot of dashboards or multiple teams using the same Grafana instance. + +## How To Create A Folder + +- Create a folder by using the Create Folder link in the side menu. + + ![](/img/docs/v50/create_folder_menu.png) + +- Use the create Folder button on the Manage Dashboards page. + +- When saving a dashboard, you can either choose a folder for the dashboard to be saved in or create a new folder (coming in 5.0 beta) + +On the Create Folder page, fill in a unique name for the folder and press Create. + +![](/img/docs/v50/create_folder_page.png) + +## Manage Dashboards + +There is a new Manage Dashboards page where you can carry out a variety of tasks: + +- create a folder +- create a dashboard +- move dashboards into folders +- delete multiple dashboards +- navigate to a folder page (where you can set permissions for a folder and/or its dashboards) + +There is a new option in the Dashboards menu for the Manage Dashboards page: + + ![](/img/docs/v50/manage_dashboard_menu.png) + +Here you can manage your dashboards: + +![](/img/docs/v50/manage_dashboards_page.png) + +Or you can go directly to a Dashboard Folder page via Dashboard Search by clicking on the cog icon: + +![](/img/docs/v50/go_to_dashboard_folder_page.png) + +## Dashboard Folder Page + +The Dashboard Folder Page is similar to the Manage Dashboards page and is where you can carry out the following tasks: + +- allows you to move or delete dashboards in a folder. +- rename a folder (under the Settings tab). +- set permissions on the whole folder. +- set permissions on a single dashboard. + +## Dashboard Permissions (Not enabled in Grafana 5.0 alpha) + +An Access Control List (ACL) model is used for permissions on Dashboard Folders. An individual user can be assigned permissions on a folder or a Team. + +The permissions that can be assigned for a folder are: View, Edit, Admin. + +The default is that: + +- everyone has access to a folder and that their permissions depend on their user role (Viewer, Editor or Admin). +- An Admin or Editor can remove the default access for everyone and can then assign a user or team to a Dashboard Folder. +- Teams make it easier to assign permissions for multiple users to multiple dashboards. + +Other Dashboard Folder rules: + +- Users with the Admin and Editor role are allowed to create new Dashboard Folders. +- Users with the Viewer role are not allowed to create new Dashboard Folders. +- Editors who are owners and Admins can assign permissions to users or teams for Dashboard Folders. +- Default permissions can be removed except for the Admin permissions (View, Edit). + +### Limiting Permissions on a Folder + +To limit permissions on a folder or dashboard: + +1. go to the permissions tab on the Dashboard Folder page +2. remove the default permissions (Everyone with Editor Role / Everyone with Viewer Role) +3. Give a team or user specific permissions. For example: `frontend-team can edit` and `ops-team can view`. + +Remember that users with the Admin role will always have permission to all folders and dashboards. + +## Teams (Not enabled in Grafana 5.0 alpha) + +Teams is a new concept for Grafana 5.0. A team is a group of users that can be assigned permissions on a dashboard folder or a dashboard. + +How Teams Work: + +- Admins can create teams. +- No hierarchies. Teams cannot contain teams. +- If a user belongs to multiple teams, their permissions are merged to give them the highest permission possible for a dashboard folder or dashboard. From 45cd3730a7b6586e0d7cf2c2439ff61e46251bcf Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Thu, 25 Jan 2018 11:24:49 +0100 Subject: [PATCH 02/58] added whats new v5, changed link in notifications, removed row from getting started --- docs/sources/alerting/notifications.md | 2 +- docs/sources/guides/getting_started.md | 30 +++++++++++++------------- docs/sources/guides/whats-new-in-v5.md | 30 ++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 16 deletions(-) create mode 100644 docs/sources/guides/whats-new-in-v5.md diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md index 4bd56aae01d..2aae066eae6 100644 --- a/docs/sources/alerting/notifications.md +++ b/docs/sources/alerting/notifications.md @@ -20,7 +20,7 @@ to add and configure a `notification` channel (can be email, PagerDuty or other ## Notification Channel Setup -{{< imgbox max-width="40%" img="/img/docs/v43/alert_notifications_menu.png" caption="Alerting Notification Channels" >}} +{{< imgbox max-width="30%" img="/img/docs/v5/alerts-notifications-menu.png" caption="Alerting Notification Channels" >}} On the Notification Channels page hit the `New Channel` button to go the page where you can configure and setup a new Notification Channel. diff --git a/docs/sources/guides/getting_started.md b/docs/sources/guides/getting_started.md index 5c02042481f..5a484174537 100644 --- a/docs/sources/guides/getting_started.md +++ b/docs/sources/guides/getting_started.md @@ -24,38 +24,38 @@ Read the [Basic Concepts](/guides/basic_concepts) document to get a crash course ### Top header -Let's start with creating a new Dashboard. You can find the new Dashboard link on the right side of the Dashboard picker. You now have a blank Dashboard. +Let's start with creating a new Dashboard. You can find the new Dashboard link on the right side of the Dashboard picker. You now have a blank Dashboard. - + 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 ![](/img/docs/v45/metrics_tab.png) -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. @@ -63,7 +63,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. ![](/img/docs/animated_gifs/drag_drop.gif) diff --git a/docs/sources/guides/whats-new-in-v5.md b/docs/sources/guides/whats-new-in-v5.md new file mode 100644 index 00000000000..affa86be264 --- /dev/null +++ b/docs/sources/guides/whats-new-in-v5.md @@ -0,0 +1,30 @@ ++++ +title = "What's New in Grafana v5.0" +description = "Feature & improvement highlights for Grafana v5.0" +keywords = ["grafana", "new", "documentation", "5.0"] +type = "docs" +[menu.docs] +name = "Version 5.0" +identifier = "v5.0" +parent = "whatsnew" +weight = -6 ++++ + +# What's New in Grafana v5.0 + +Grafana v5.0 is here and with it comes the new feature: Dashboard folders. Grafana has never looked this good. + +## UX improvements + +Grafana v5.0 brings big changes to UX/UI. +The grid system for the dashboards are more flexible than ever. +Rows takes on a new role. +There is now a dashboard settings page. + + + +## Dashboard folders + +The big new feature that comes with Grafana v5.0 is Dashboard folders. Now you can organize your dashbords into folders. + +## Teams \ No newline at end of file From 4294009b0b170c007e5fdfcc07cd91dd31033c6a Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 26 Jan 2018 10:58:31 +0100 Subject: [PATCH 03/58] changed some img-links, updated text for annotated img, more work on whats new in v5.0 --- docs/sources/alerting/notifications.md | 2 +- docs/sources/guides/getting_started.md | 4 ++-- docs/sources/guides/whats-new-in-v5.md | 20 +++++++++++++------- docs/sources/reference/annotations.md | 3 ++- docs/sources/reference/playlist.md | 2 +- docs/sources/reference/search.md | 12 +++++++----- docs/sources/reference/templating.md | 17 ++++++++++------- 7 files changed, 36 insertions(+), 24 deletions(-) diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md index 2aae066eae6..896aa769b72 100644 --- a/docs/sources/alerting/notifications.md +++ b/docs/sources/alerting/notifications.md @@ -20,7 +20,7 @@ to add and configure a `notification` channel (can be email, PagerDuty or other ## Notification Channel Setup -{{< imgbox max-width="30%" img="/img/docs/v5/alerts-notifications-menu.png" caption="Alerting Notification Channels" >}} +{{< imgbox max-width="30%" img="/img/docs/v5/alerts_notifications_menu.png" caption="Alerting Notification Channels" >}} On the Notification Channels page hit the `New Channel` button to go the page where you can configure and setup a new Notification Channel. diff --git a/docs/sources/guides/getting_started.md b/docs/sources/guides/getting_started.md index 5a484174537..9a887ac0aa0 100644 --- a/docs/sources/guides/getting_started.md +++ b/docs/sources/guides/getting_started.md @@ -26,7 +26,7 @@ Read the [Basic Concepts](/guides/basic_concepts) document to get a crash course Let's start with creating a new Dashboard. You can find the new Dashboard link on the right side of the Dashboard picker. You now have a blank Dashboard. - + The image above shows you the top header for a Dashboard. @@ -42,7 +42,7 @@ The image above shows you the top header for a Dashboard. 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. diff --git a/docs/sources/guides/whats-new-in-v5.md b/docs/sources/guides/whats-new-in-v5.md index affa86be264..97c8efb8a48 100644 --- a/docs/sources/guides/whats-new-in-v5.md +++ b/docs/sources/guides/whats-new-in-v5.md @@ -12,19 +12,25 @@ weight = -6 # What's New in Grafana v5.0 -Grafana v5.0 is here and with it comes the new feature: Dashboard folders. Grafana has never looked this good. +Grafana v5.0 is here and with it comes a new dashboard engine and the new feature: Dashboard folders. Grafana has never looked this good. + +## New dashboard engine + +The new grid system for the dashboards are more flexible than ever and are no longer dependent on rows. The panels can be organized in any way you want simply by dragging and dropping and stretching and contracting. + +The rows are still there but have taken on a new role. ## UX improvements Grafana v5.0 brings big changes to UX/UI. -The grid system for the dashboards are more flexible than ever. -Rows takes on a new role. There is now a dashboard settings page. - - ## Dashboard folders -The big new feature that comes with Grafana v5.0 is Dashboard folders. Now you can organize your dashbords into folders. +The big new feature that comes with Grafana v5.0 is Dashboard folders. Now you can organize your dashbords into folders which is very useful if you have a lot of dashboards or multiple teams using the same Grafana instance. -## Teams \ No newline at end of file +Each folder has its own page where you can set permisions for the folder or single dashboards within the folder. Here you can also delete and move dashboards. + +## Teams + +Teams are a new concept for Grafana. Teams are simply a group of users that can be given persmisions for folders or dashboards. Only an admin can create teams. \ No newline at end of file diff --git a/docs/sources/reference/annotations.md b/docs/sources/reference/annotations.md index de118f37d46..bfc104ef522 100644 --- a/docs/sources/reference/annotations.md +++ b/docs/sources/reference/annotations.md @@ -54,7 +54,8 @@ Annotation events are fetched via annotation queries. To add a new annotation qu open the dashboard settings menu, then select `Annotations`. This will open the dashboard annotations settings view. To create a new annotation query hit the `New` button. -![](/img/docs/annotations/new_query.png) + +{{< docs-imagebox img="/img/docs/v50/annotation_new_query.png" max-width="600px" >}} Specify a name for the annotation query. This name is given to the toggle (checkbox) that will allow you to enable/disable showing annotation events from this query. For example you might have two 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..90ab33b09d5 100644 --- a/docs/sources/reference/search.md +++ b/docs/sources/reference/search.md @@ -12,12 +12,14 @@ weight = 5 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. - + -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. diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md index 36308adf52f..348b4ba71a6 100644 --- a/docs/sources/reference/templating.md +++ b/docs/sources/reference/templating.md @@ -1,20 +1,21 @@ +++ -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 +44,8 @@ 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 +135,8 @@ 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. -![](/img/docs/v4/variable_dropdown_tags.png) + +{{< docs-imagebox img="/img/docs/v50/variable_dropdown_tags.png" max-width="300px" >}} ### Interval variables From 3fa2f8a411cfeae3854cbbcc30d715349b0e7834 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Mon, 29 Jan 2018 15:45:28 +0100 Subject: [PATCH 04/58] changed img-link for timerange imgs and some text --- docs/sources/reference/timerange.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/sources/reference/timerange.md b/docs/sources/reference/timerange.md index a4d6fc62336..ec626aa3113 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. From ec3ba86a02326c0c5129b2f75781697c94341abe Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 30 Jan 2018 09:20:55 +0100 Subject: [PATCH 05/58] replaced img in export_import and sharing --- docs/sources/guides/whats-new-in-v5.md | 4 +++- docs/sources/reference/export_import.md | 10 +++++----- docs/sources/reference/sharing.md | 6 +++--- docs/sources/reference/templating.md | 3 --- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/docs/sources/guides/whats-new-in-v5.md b/docs/sources/guides/whats-new-in-v5.md index 97c8efb8a48..ca66a3d7f3f 100644 --- a/docs/sources/guides/whats-new-in-v5.md +++ b/docs/sources/guides/whats-new-in-v5.md @@ -18,13 +18,15 @@ Grafana v5.0 is here and with it comes a new dashboard engine and the new featur The new grid system for the dashboards are more flexible than ever and are no longer dependent on rows. The panels can be organized in any way you want simply by dragging and dropping and stretching and contracting. -The rows are still there but have taken on a new role. +The rows are still there but have taken on a new role. You now use rows to group and hide your panels. You still organize your panels as you want in rows. ## UX improvements Grafana v5.0 brings big changes to UX/UI. There is now a dashboard settings page. +{{< docs-imagebox img="/img/docs/v50/v5_dashboard_settings.png" max-width="700px" >}} + ## Dashboard folders The big new feature that comes with Grafana v5.0 is Dashboard folders. Now you can organize your dashbords into folders which is very useful if you have a lot of dashboards or multiple teams using the same Grafana instance. diff --git a/docs/sources/reference/export_import.md b/docs/sources/reference/export_import.md index 4c2d5faa3d3..747070be286 100644 --- a/docs/sources/reference/export_import.md +++ b/docs/sources/reference/export_import.md @@ -15,9 +15,9 @@ Grafana Dashboards can easily be exported and imported, either from the UI or fr Dashboards are exported in Grafana JSON format, and contain everything you need (layout, variables, styles, data sources, queries, etc)to import the dashboard at a later time. -The export feature is accessed from the share menu. +The export feature is accessed in the share window which you open by clicking the share button in the dashboard menu. - +{{< docs-imagebox img="/img/docs/v50/export_modal.png" >}} ### 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" >}} 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" >}} 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" >}} ## Import & Sharing with Grafana 2.x or 3.0 diff --git a/docs/sources/reference/sharing.md b/docs/sources/reference/sharing.md index badd3b5712a..d61d4167b5a 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. -![](/img/docs/v4/share_panel_modal.png) +{{< 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 -![](/img/docs/v4/export_panel_data.png) +{{< 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 348b4ba71a6..3a15b4ed7d1 100644 --- a/docs/sources/reference/templating.md +++ b/docs/sources/reference/templating.md @@ -14,7 +14,6 @@ Variables allows for more interactive and dynamic dashboards. Instead of hard-co 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? @@ -44,7 +43,6 @@ 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. @@ -135,7 +133,6 @@ 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 From c29e75e9bc1ab93e478e083e8b5bf7397313cfd5 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 30 Jan 2018 10:51:50 +0100 Subject: [PATCH 06/58] changed img for shortcuts --- docs/sources/alerting/notifications.md | 2 +- docs/sources/features/shortcuts.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md index 896aa769b72..3b348427162 100644 --- a/docs/sources/alerting/notifications.md +++ b/docs/sources/alerting/notifications.md @@ -20,7 +20,7 @@ to add and configure a `notification` channel (can be email, PagerDuty or other ## Notification Channel Setup -{{< imgbox max-width="30%" img="/img/docs/v5/alerts_notifications_menu.png" caption="Alerting Notification Channels" >}} +{{< imgbox max-width="30%" img="/img/docs/v50/alerts_notifications_menu.png" caption="Alerting Notification Channels" >}} On the Notification Channels page hit the `New Channel` button to go the page where you can configure and setup a new Notification Channel. diff --git a/docs/sources/features/shortcuts.md b/docs/sources/features/shortcuts.md index caad521446e..66baddcc91c 100644 --- a/docs/sources/features/shortcuts.md +++ b/docs/sources/features/shortcuts.md @@ -8,7 +8,7 @@ weight = 7 # Keyboard shortcuts -{{< docs-imagebox img="/img/docs/v4/shortcuts.png" max-width="20rem" class="docs-image--right" >}} +{{< docs-imagebox img="/img/docs/v50/shortcuts.png" max-width="20rem" class="docs-image--right" >}} Grafana v4 introduces a number of really powerful keyboard shortcuts. You can now focus a panel by hovering over it with your mouse. With a panel focused you can simple hit `e` to toggle panel From 40832e6d7108c009e711ebda79ed9841bf6e5d13 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 30 Jan 2018 15:11:48 +0100 Subject: [PATCH 07/58] docs: adds more info about whats new in v5 --- docs/sources/guides/whats-new-in-v5.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/sources/guides/whats-new-in-v5.md b/docs/sources/guides/whats-new-in-v5.md index ca66a3d7f3f..8779ef4241f 100644 --- a/docs/sources/guides/whats-new-in-v5.md +++ b/docs/sources/guides/whats-new-in-v5.md @@ -35,4 +35,22 @@ Each folder has its own page where you can set permisions for the folder or sing ## Teams -Teams are a new concept for Grafana. Teams are simply a group of users that can be given persmisions for folders or dashboards. Only an admin can create teams. \ No newline at end of file +Teams are a new concept for Grafana. Teams are simply a group of users that can be given permissions for folders or dashboards. Only an admin can create teams. + +# Dashboard model + +We are introducing a new identifier in the dashboard JSON model. The new identifier will be a X long uid. We are also changing the route for getting dashboards to use this id instead (we will keep supporting the old route for backward compatibility). This will make it possible to change the title on dashboards without breaking links. Sharing dashboards between instances become much easier since the uid is unique (unique enough) and the old numeric id always depends on the instance and might cause a conflict. This might seem like a small change, but we are incredibly excited about it since it will make it much easier to manage, collaborate and navigate between dashboards + +# Provisioning Grafana from configuration + +In previous versions of Grafana, you could use the API for provisioning. But that required the service to be running before you started creating dashboards and you also needed to set up credentials for authentication. In 5.0 we decided to improve this experience and enable people to provision using config files instead. Not only will this make gitops more natural, and it will also allow people to run Grafana as a stateless application. + +In 5.0 we added support for provisioning data sources and dashboards. We will add support for provisioning more parts of Grafana in the future. + +## Data sources + +It's now possible to create data sources in Grafana only using config files. These data sources are by default not editable from the Grafana GUI. Its also possible to update and delete data sources from the config, which makes it possible to manage data sources only thru configuration. More info in the [data source provisioning docs](/administration/provisioning/#datasources) + +## Dashboards + +We also deprecated [dashboard.json] in favor of our new dashboard provisioner that keeps dashboards on disk in sync with Grafana. The dashboard provisioner have multiple advantages over the old [dashboard.json] feature. Instead of storing the dashboard in memory we now insert the dashboard into the database, which makes it possible to use it with dashboard folders, permissions, built-in annotations and other features in Grafana that expects the dashboards to exist in the database. More info in the [dashboard provisioning docs](/administration/provisioning/#dashboards) \ No newline at end of file From 411c81fee8532e02f2530d504e1fb65d5b6a4ca4 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 30 Jan 2018 15:34:09 +0100 Subject: [PATCH 08/58] new gifs for search --- docs/sources/reference/search.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/sources/reference/search.md b/docs/sources/reference/search.md index 90ab33b09d5..b3929e8c906 100644 --- a/docs/sources/reference/search.md +++ b/docs/sources/reference/search.md @@ -25,7 +25,7 @@ When using only a keyboard, you can use your keyboard arrow keys to navigate the ## 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. @@ -40,21 +40,27 @@ 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: - + + + + + + + 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 + From 8ecf2d127c4f01e0680a258f1d51957028a708a9 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 30 Jan 2018 17:12:12 +0100 Subject: [PATCH 09/58] fixed width of images and removed gifs and fixed text a bit in search --- docs/sources/guides/getting_started.md | 2 +- docs/sources/reference/export_import.md | 8 +++---- docs/sources/reference/search.md | 31 ++++--------------------- docs/sources/reference/timerange.md | 2 +- 4 files changed, 11 insertions(+), 32 deletions(-) diff --git a/docs/sources/guides/getting_started.md b/docs/sources/guides/getting_started.md index 9a887ac0aa0..e6ae6d8dc5e 100644 --- a/docs/sources/guides/getting_started.md +++ b/docs/sources/guides/getting_started.md @@ -42,7 +42,7 @@ The image above shows you the top header for a Dashboard. 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. diff --git a/docs/sources/reference/export_import.md b/docs/sources/reference/export_import.md index 747070be286..31f32d890f6 100644 --- a/docs/sources/reference/export_import.md +++ b/docs/sources/reference/export_import.md @@ -17,7 +17,7 @@ Dashboards are exported in Grafana JSON format, and contain everything you need The export feature is accessed in the share window which you open by clicking the share button in the dashboard menu. -{{< docs-imagebox img="/img/docs/v50/export_modal.png" >}} +{{< 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" >}} +{{< 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" >}} +{{< 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" >}} +{{< 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/search.md b/docs/sources/reference/search.md index b3929e8c906..1bf6fd53e52 100644 --- a/docs/sources/reference/search.md +++ b/docs/sources/reference/search.md @@ -10,9 +10,9 @@ 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. `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. @@ -25,9 +25,7 @@ When using only a keyboard, you can use your keyboard arrow keys to navigate the ## 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 @@ -40,27 +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**. - - - +**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/timerange.md b/docs/sources/reference/timerange.md index ec626aa3113..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). From 8fed56e3344f93e78f254e4d2a50a451b653457f Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 30 Jan 2018 16:00:16 +0100 Subject: [PATCH 10/58] test: fixes failing test in go1.10 --- .../datasource/wrapper/datasource_plugin_wrapper_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/plugins/datasource/wrapper/datasource_plugin_wrapper_test.go b/pkg/plugins/datasource/wrapper/datasource_plugin_wrapper_test.go index c85cb6cea52..834e8238e3a 100644 --- a/pkg/plugins/datasource/wrapper/datasource_plugin_wrapper_test.go +++ b/pkg/plugins/datasource/wrapper/datasource_plugin_wrapper_test.go @@ -75,7 +75,7 @@ func TestMappingRowValue(t *testing.T) { boolRowValue, _ := dpw.mapRowValue(&datasource.RowValue{Kind: datasource.RowValue_TYPE_BOOL, BoolValue: true}) haveBool, ok := boolRowValue.(bool) if !ok || haveBool != true { - t.Fatalf("Expected true, was %s", haveBool) + t.Fatalf("Expected true, was %v", haveBool) } intRowValue, _ := dpw.mapRowValue(&datasource.RowValue{Kind: datasource.RowValue_TYPE_INT64, Int64Value: 42}) From 1cc5349bfda744cb98c657feabaa9a243d08821a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 30 Jan 2018 15:21:43 +0100 Subject: [PATCH 11/58] docs: fixed order of sidemenu --- docs/sources/features/index.md | 2 +- docs/sources/guides/basic_concepts.md | 1 + docs/sources/guides/getting_started.md | 1 + docs/sources/guides/index.md | 2 +- docs/sources/index.md | 4 ---- docs/sources/installation/index.md | 1 + docs/sources/{features => }/whatsnew/index.md | 2 +- 7 files changed, 6 insertions(+), 7 deletions(-) rename docs/sources/{features => }/whatsnew/index.md (90%) diff --git a/docs/sources/features/index.md b/docs/sources/features/index.md index 1352e9f0576..d7fae41682b 100644 --- a/docs/sources/features/index.md +++ b/docs/sources/features/index.md @@ -5,7 +5,7 @@ type = "docs" [menu.docs] name = "Features" identifier = "features" -weight = 3 +weight = 4 +++ diff --git a/docs/sources/guides/basic_concepts.md b/docs/sources/guides/basic_concepts.md index cb64b105349..b710a227a79 100644 --- a/docs/sources/guides/basic_concepts.md +++ b/docs/sources/guides/basic_concepts.md @@ -7,6 +7,7 @@ type = "docs" name = "Basic Concepts" identifier = "basic_concepts" parent = "guides" +weight = 2 +++ # Basic Concepts diff --git a/docs/sources/guides/getting_started.md b/docs/sources/guides/getting_started.md index 9a887ac0aa0..0749a23768b 100644 --- a/docs/sources/guides/getting_started.md +++ b/docs/sources/guides/getting_started.md @@ -8,6 +8,7 @@ aliases = ["/guides/gettingstarted"] name = "Getting Started" identifier = "getting_started_guide" parent = "guides" +weight = 1 +++ # Getting started diff --git a/docs/sources/guides/index.md b/docs/sources/guides/index.md index c80dd624624..2ea09142880 100644 --- a/docs/sources/guides/index.md +++ b/docs/sources/guides/index.md @@ -4,6 +4,6 @@ type = "docs" [menu.docs] name = "Getting Started" identifier = "guides" -weight = 2 +weight = 3 +++ diff --git a/docs/sources/index.md b/docs/sources/index.md index 7a431e29692..3536eb5437e 100644 --- a/docs/sources/index.md +++ b/docs/sources/index.md @@ -4,10 +4,6 @@ description = "Install guide for Grafana" keywords = ["grafana", "installation", "documentation"] type = "docs" aliases = ["v1.1", "guides/reference/admin"] -[menu.docs] -name = "Welcome to the Docs" -identifier = "root" -weight = -1 +++ # Welcome to the Grafana Documentation diff --git a/docs/sources/installation/index.md b/docs/sources/installation/index.md index 47d52817b64..a86f426dc13 100644 --- a/docs/sources/installation/index.md +++ b/docs/sources/installation/index.md @@ -7,6 +7,7 @@ aliases = ["installation/installation/", "v2.1/installation/install/"] [menu.docs] name = "Installation" identifier = "installation" +weight = 1 +++ ## Installing Grafana diff --git a/docs/sources/features/whatsnew/index.md b/docs/sources/whatsnew/index.md similarity index 90% rename from docs/sources/features/whatsnew/index.md rename to docs/sources/whatsnew/index.md index 30357af4668..df472f07093 100644 --- a/docs/sources/features/whatsnew/index.md +++ b/docs/sources/whatsnew/index.md @@ -3,7 +3,7 @@ title = "What's New in Grafana" [menu.docs] name = "What's New In Grafana" identifier = "whatsnew" -weight = 2 +weight = 3 +++ From 9a6bfb61a1383b5774800650b5240b8d43cdead4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 30 Jan 2018 16:35:59 +0100 Subject: [PATCH 12/58] docs: updated version --- docs/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/VERSION b/docs/VERSION index b570a0ac21b..5e0a0f1d665 100644 --- a/docs/VERSION +++ b/docs/VERSION @@ -1 +1 @@ -v4.3 +v5.0 From 88e4f199091d771f3e08981cb4304a070937753f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 30 Jan 2018 17:36:58 +0100 Subject: [PATCH 13/58] docs: progress on whats new article --- docs/sources/guides/whats-new-in-v2-1.md | 5 -- docs/sources/guides/whats-new-in-v2-5.md | 5 -- docs/sources/guides/whats-new-in-v2-6.md | 5 -- docs/sources/guides/whats-new-in-v2.md | 5 -- docs/sources/guides/whats-new-in-v5.md | 106 ++++++++++++++++++----- 5 files changed, 85 insertions(+), 41 deletions(-) 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/guides/whats-new-in-v5.md b/docs/sources/guides/whats-new-in-v5.md index 8779ef4241f..045061df041 100644 --- a/docs/sources/guides/whats-new-in-v5.md +++ b/docs/sources/guides/whats-new-in-v5.md @@ -12,45 +12,109 @@ weight = -6 # What's New in Grafana v5.0 -Grafana v5.0 is here and with it comes a new dashboard engine and the new feature: Dashboard folders. Grafana has never looked this good. +This is the most substantial update that Grafana has ever seen. This article will detail the major new features and enhancements. -## New dashboard engine +- [New Dashboard Layout Engine]({{< relref "#new-dashboard-layout-engine" >}}) enables a much easier drag, drop and resize experiance and new types of layouts. +- [New UX]({{< relref "#new-ux-layout-engine" >}}). The UI has big improvements in both look and function. +- [New Light Theme]({{< relref "#new-light-theme" >}}) is now looking really nice. +- [Dashboard Folders]({{< relref "#dashboard-folders" >}}) helps you keep your dashboards organized. +- [Permissions]({{< relref "#dashboard-folders" >}}) on folders and dashboards helps managing a large Grafana installation. +- [Group users into teams]({{< relref "#teams" >}}) and use them in the new permission system. +- [Datasource provisioning]({{< relref "#data-sources" >}}) makes it possible to setup data sources via config files. +- [Dashboard provisioning]({{< relref "#dashboards" >}}) makes it possible to setup dashboards via config files. -The new grid system for the dashboards are more flexible than ever and are no longer dependent on rows. The panels can be organized in any way you want simply by dragging and dropping and stretching and contracting. +### Video showing new features -The rows are still there but have taken on a new role. You now use rows to group and hide your panels. You still organize your panels as you want in rows. + +
-## UX improvements +## New Dashboard Layout Engine -Grafana v5.0 brings big changes to UX/UI. -There is now a dashboard settings page. +{{< docs-imagebox img="/img/docs/v50/new_grid.png" max-width="700px" class="docs-image--right">}} -{{< docs-imagebox img="/img/docs/v50/v5_dashboard_settings.png" max-width="700px" >}} +The new dashboard layout engine allows for much easier movement & sizing of panels as other panels now move out of the way in +a very intutive way. No longer do you need to use rows to create layouts as panels are sized independantly. This opens +up many new types of layouts where panels of different heights can be aligned easily. Checkout the new grid in the video +above or on the [play site](http://play.grafana.org). All your existing dashboards will automatically migrate to the +new position system and look close to identical. The new panel position makes dashboards saved in v5.0 not compatible +with older versions of Grafana. -## Dashboard folders +
-The big new feature that comes with Grafana v5.0 is Dashboard folders. Now you can organize your dashbords into folders which is very useful if you have a lot of dashboards or multiple teams using the same Grafana instance. +## New UX -Each folder has its own page where you can set permisions for the folder or single dashboards within the folder. Here you can also delete and move dashboards. +{{< docs-imagebox img="/img/docs/v50/new_ux_nav.png" max-width="700px" class="docs-image--right" >}} + +Almost every page has been seen significant UX improvements. All non dashboard pages has new tab based layout that improves navigation between pages. +The side menu has also changed quite a bit. You can still hide the side menu completly if you click on the Grafana +logo. + +
+ +### Dashboard Settings + +{{< docs-imagebox img="/img/docs/v50/dashboard_settings.png" max-width="700px" class="docs-image--right" >}} +Dashboard has new header toolbar look where buttons and actions are now all moved to the right. All the dashboard +settings views has been combined with a side nav which allows you to easily move between different setting categories. + +
+ +## New Light Theme + +{{< docs-imagebox img="/img/docs/v50/new_white_theme.png" max-width="700px" class="docs-image--right" >}} + +The light theme has seen major revision in v5. This theme has not seen a lot of love in recent years and we felt it was time to +rework it and give it a major overhaul. + +
+ +## Dashboard Folders + +{{< docs-imagebox img="/img/docs/v50/new_search.png" max-width="700px" class="docs-image--right" >}} + +The big new feature that comes with Grafana v5.0 is dashboard folders. Now you can organize your dashbords in folders +which is very useful if you have a lot of dashboards or multiple teams. + +- New search design that adds expandable sections for each folder, starred & recently viewed dashboards. +- New manage dashboard pages that enables batch actions and views for folder permissions. +- Set permissions on folders and have dashboards inherit the permissions. ## Teams -Teams are a new concept for Grafana. Teams are simply a group of users that can be given permissions for folders or dashboards. Only an admin can create teams. +Teams is a new concept in Grafana v5. Teams are simply a group of users that can be given persmisions for folders or dashboards. Only an admin can create teams. +We hope to do more with teams in future releases like integration with LDAP & a team landing page. -# Dashboard model +## Permissions -We are introducing a new identifier in the dashboard JSON model. The new identifier will be a X long uid. We are also changing the route for getting dashboards to use this id instead (we will keep supporting the old route for backward compatibility). This will make it possible to change the title on dashboards without breaking links. Sharing dashboards between instances become much easier since the uid is unique (unique enough) and the old numeric id always depends on the instance and might cause a conflict. This might seem like a small change, but we are incredibly excited about it since it will make it much easier to manage, collaborate and navigate between dashboards +TODO -# Provisioning Grafana from configuration +# Provisioning from configuration -In previous versions of Grafana, you could use the API for provisioning. But that required the service to be running before you started creating dashboards and you also needed to set up credentials for authentication. In 5.0 we decided to improve this experience and enable people to provision using config files instead. Not only will this make gitops more natural, and it will also allow people to run Grafana as a stateless application. +In previous versions of Grafana you could only use the API for provisioning data sources and dashboards. +But that required the service to be running before you started creating dashboards and you also needed to +set up credentials for the HTTP API. In 5.0 we decided to improve this experience by adding a new active +provisioning system that uses config files. This will make gitops more natural as datasources and dashboards can +be defined in via files that can be version controlled. -In 5.0 we added support for provisioning data sources and dashboards. We will add support for provisioning more parts of Grafana in the future. +### Data sources -## Data sources +It's now possible to setup data sources in Grafana using config files. These data sources are by default not editable from the Grafana GUI. +Its also possible to update and delete data sources from the config file. More info in the [data source provisioning docs](/administration/provisioning/#datasources). -It's now possible to create data sources in Grafana only using config files. These data sources are by default not editable from the Grafana GUI. Its also possible to update and delete data sources from the config, which makes it possible to manage data sources only thru configuration. More info in the [data source provisioning docs](/administration/provisioning/#datasources) +### Dashboards + +We also deprecated the [dashboard.json] in favor of our new dashboard provisioner that keeps dashboards on disk +in sync with dashboards in Grafana's database. The dashboard provisioner have multiple advantages over the old +[dashboard.json] feature. Instead of storing the dashboard in memory we now insert the dashboard into the database, +which makes it possible to star them, use one as home dashboard, set permissions and other features in Grafana that +expects the dashboards to exist in the database. More info in the [dashboard provisioning docs](/administration/provisioning/#dashboards) + +# Dashboard model & API + +We are introducing a new identifier (`uid`) in the dashboard JSON model. The new identifier will be a 9-12 character long unique id. +We are also changing the route for getting dashboards to use this `uid` instead of the slug that that the current route & API are using. +We will keep supporting the old route for backward compatibility. This will make it possible to change the title on dashboards without breaking links. +Sharing dashboards between instances become much easier since the uid is unique (unique enough). This might seem like a small change, +but we are incredibly excited about it since it will make it much easier to manage, collaborate and navigate between dashboards. -## Dashboards -We also deprecated [dashboard.json] in favor of our new dashboard provisioner that keeps dashboards on disk in sync with Grafana. The dashboard provisioner have multiple advantages over the old [dashboard.json] feature. Instead of storing the dashboard in memory we now insert the dashboard into the database, which makes it possible to use it with dashboard folders, permissions, built-in annotations and other features in Grafana that expects the dashboards to exist in the database. More info in the [dashboard provisioning docs](/administration/provisioning/#dashboards) \ No newline at end of file From c1cdcd3bdab6d9d378f8555472b4ae33e8496019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 31 Jan 2018 08:47:26 +0100 Subject: [PATCH 14/58] docs: updated whatsnew --- .../prometheus/img/prometheus_logo.svg | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/img/prometheus_logo.svg b/public/app/plugins/datasource/prometheus/img/prometheus_logo.svg index 29005ec3860..4c4448862e6 100644 --- a/public/app/plugins/datasource/prometheus/img/prometheus_logo.svg +++ b/public/app/plugins/datasource/prometheus/img/prometheus_logo.svg @@ -1,19 +1 @@ - - - - - - - - - - + \ No newline at end of file From b1a936075650f1dd8907416ccefc275836395b0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 31 Jan 2018 11:26:00 +0100 Subject: [PATCH 15/58] docs: updated whats new --- docs/sources/guides/whats-new-in-v5.md | 40 +++++++++++++------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/sources/guides/whats-new-in-v5.md b/docs/sources/guides/whats-new-in-v5.md index 045061df041..ff73d8aa2b3 100644 --- a/docs/sources/guides/whats-new-in-v5.md +++ b/docs/sources/guides/whats-new-in-v5.md @@ -33,7 +33,7 @@ This is the most substantial update that Grafana has ever seen. This article wil {{< docs-imagebox img="/img/docs/v50/new_grid.png" max-width="700px" class="docs-image--right">}} The new dashboard layout engine allows for much easier movement & sizing of panels as other panels now move out of the way in -a very intutive way. No longer do you need to use rows to create layouts as panels are sized independantly. This opens +a very intuitive way. No longer do you need to use rows to create layouts as panels are sized independently. This opens up many new types of layouts where panels of different heights can be aligned easily. Checkout the new grid in the video above or on the [play site](http://play.grafana.org). All your existing dashboards will automatically migrate to the new position system and look close to identical. The new panel position makes dashboards saved in v5.0 not compatible @@ -45,9 +45,7 @@ with older versions of Grafana. {{< docs-imagebox img="/img/docs/v50/new_ux_nav.png" max-width="700px" class="docs-image--right" >}} -Almost every page has been seen significant UX improvements. All non dashboard pages has new tab based layout that improves navigation between pages. -The side menu has also changed quite a bit. You can still hide the side menu completly if you click on the Grafana -logo. +Almost every page has been seen significant UX improvements. All pages (except dashboard) has new tab-based layout that improves navigation between pages. The side menu has also changed quite a bit. You can still hide the side menu completely if you click on the Grafana logo.
@@ -63,8 +61,7 @@ settings views has been combined with a side nav which allows you to easily move {{< docs-imagebox img="/img/docs/v50/new_white_theme.png" max-width="700px" class="docs-image--right" >}} -The light theme has seen major revision in v5. This theme has not seen a lot of love in recent years and we felt it was time to -rework it and give it a major overhaul. +This theme has not seen a lot of love in recent years and we felt it was time to rework it and give it a major overhaul. We are very happy with the result.
@@ -72,49 +69,52 @@ rework it and give it a major overhaul. {{< docs-imagebox img="/img/docs/v50/new_search.png" max-width="700px" class="docs-image--right" >}} -The big new feature that comes with Grafana v5.0 is dashboard folders. Now you can organize your dashbords in folders +The big new feature that comes with Grafana v5.0 is dashboard folders. Now you can organize your dashboards in folders which is very useful if you have a lot of dashboards or multiple teams. - New search design that adds expandable sections for each folder, starred & recently viewed dashboards. -- New manage dashboard pages that enables batch actions and views for folder permissions. +- New manage dashboard pages that enable batch actions and views for folder settings & permissions. - Set permissions on folders and have dashboards inherit the permissions. ## Teams -Teams is a new concept in Grafana v5. Teams are simply a group of users that can be given persmisions for folders or dashboards. Only an admin can create teams. +A team is a new concept in Grafana v5. They are simply a group of users that can be then be used in the new permission system for dashboards and folders. Only an admin can create teams. We hope to do more with teams in future releases like integration with LDAP & a team landing page. ## Permissions -TODO +{{< docs-imagebox img="/img/docs/v50/folder_permissions.png" max-width="700px" class="docs-image--right" >}} + +You can assign permissions to folders and dashboards. The default user role-based permissions can be removed and replaced with specific teams or users enabling more control over what a user can see & edit. + +
# Provisioning from configuration -In previous versions of Grafana you could only use the API for provisioning data sources and dashboards. +In previous versions of Grafana, you could only use the API for provisioning data sources and dashboards. But that required the service to be running before you started creating dashboards and you also needed to set up credentials for the HTTP API. In 5.0 we decided to improve this experience by adding a new active -provisioning system that uses config files. This will make gitops more natural as datasources and dashboards can -be defined in via files that can be version controlled. +provisioning system that uses config files. This will make GitOps more natural as data sources and dashboards can +be defined via files that can be version controlled. We hope to extend this system to later add support for users, orgs +and alerts as well. ### Data sources -It's now possible to setup data sources in Grafana using config files. These data sources are by default not editable from the Grafana GUI. -Its also possible to update and delete data sources from the config file. More info in the [data source provisioning docs](/administration/provisioning/#datasources). +Data sources can now be setup using config files. These data sources are by default not editable from the Grafana GUI. +It's also possible to update and delete data sources from the config file. More info in the [data source provisioning docs](/administration/provisioning/#datasources). ### Dashboards We also deprecated the [dashboard.json] in favor of our new dashboard provisioner that keeps dashboards on disk in sync with dashboards in Grafana's database. The dashboard provisioner have multiple advantages over the old [dashboard.json] feature. Instead of storing the dashboard in memory we now insert the dashboard into the database, -which makes it possible to star them, use one as home dashboard, set permissions and other features in Grafana that +which makes it possible to star them, use one as the home dashboard, set permissions and other features in Grafana that expects the dashboards to exist in the database. More info in the [dashboard provisioning docs](/administration/provisioning/#dashboards) # Dashboard model & API We are introducing a new identifier (`uid`) in the dashboard JSON model. The new identifier will be a 9-12 character long unique id. -We are also changing the route for getting dashboards to use this `uid` instead of the slug that that the current route & API are using. +We are also changing the route for getting dashboards to use this `uid` instead of the slug that the current route & API are using. We will keep supporting the old route for backward compatibility. This will make it possible to change the title on dashboards without breaking links. Sharing dashboards between instances become much easier since the uid is unique (unique enough). This might seem like a small change, -but we are incredibly excited about it since it will make it much easier to manage, collaborate and navigate between dashboards. - - +but we are incredibly excited about it since it will make it much easier to manage, collaborate and navigate between dashboards. \ No newline at end of file From 17c1e7bac7b6d84709681254d02ed00665143a8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 31 Jan 2018 16:07:27 +0100 Subject: [PATCH 16/58] docs: added permissions page and updated folder docs --- docs/sources/administration/permissions.md | 74 ++++++++++++++++++++ docs/sources/guides/whats-new-in-v5.md | 12 ++-- docs/sources/installation/configuration.md | 2 +- docs/sources/reference/admin.md | 42 ------------ docs/sources/reference/dashboard_folders.md | 75 +++++---------------- 5 files changed, 96 insertions(+), 109 deletions(-) create mode 100644 docs/sources/administration/permissions.md delete mode 100644 docs/sources/reference/admin.md diff --git a/docs/sources/administration/permissions.md b/docs/sources/administration/permissions.md new file mode 100644 index 00000000000..daecb1807e8 --- /dev/null +++ b/docs/sources/administration/permissions.md @@ -0,0 +1,74 @@ ++++ +title = "Permissions" +description = "Grafana user permissions" +keywords = ["grafana", "configuration", "documentation", "admin", "users", "permissions"] +type = "docs" +aliases = ["/reference/admin"] +[menu.docs] +name = "Permissions" +parent = "admin" +weight = 3 ++++ + +# Permissions + +Grafana users have permissions that are determined by their: + +- **Organization Role** (Admin, Editor, Viewer) +- Via **Team** memberships where the **Team** has been assigned specific permissions. +- Via permissions assigned directly to user (on folders or dashboards) +- The Grafana Admin (i.e. Super Admin) user flag. + +## Organization Roles + +Users can be belong to one or more organizations. A user's organization membership is tied to a role that defines what the user is allowed to do +in that organization. + +### Admin Role + +Can do everything scoped to the organization. For example: + +- Add & Edit data data sources. +- Add & Edit organization users & teams. +- Configure App plugins & set org settings. + +### Editor Role + +- Can create and modify dashboards & alert rules. This can be disabled on specific folders and dashboards. +- **Cannot** create or edit data sources nor invite new users. + +### Viewer Role + +- View any dashboard. This can be disabled on specific folders and dashboards. +- **Cannot** create or edit dashboards nor data sources. + +This role can be tweaked via Grafana server setting [viewers_can_edit]({{< relref "installation/configuration.md#viewers-can-edit" >}}). If you set this to true users +with **Viewer** can also make transient dashboard edits, meaning they can modify panels & queries but not save the changes (nor create new dashboards). +Useful for public Grafana installations where you want anonymous users to be able to edit panels & queries but not save or create new dashboards. + +## Grafana Admin + +This admin flag makes a user a `Super Admin`. This means they can access the `Server Admin` views where all users and organizations can be administrated. + +### Dashboard & Folder Permissions + +{{< docs-imagebox img="/img/docs/v50/folder_permissions.png" max-width="500px" class="docs-image--right" >}} + +For dashboards and dashboard folders there is a **Permissions** page that make it possible to +remove the default role based permssions for Editors and Viewers. It's here you can add and assign permissions to specific **Users** and **Teams**. + +You can assign & remove permissions for **Organization Roles**, **Users** and **Teams**. + +Permission levels: + +- **Admin**: Can edit & create dashboards and edit permissions. +- **Edit**: Can edit & create dashboards. **Cannot** edit folder/dashboard permissions. +- **View**: Can only view existing dashboars/folders. + +#### Restricting access + +The highest permission always wins so if you for example want to hide a folder or dashboard from others you need to remove the **Organization Role** based permission from the +Access Control List (ACL). + +- You cannot override permissions for users with **Org Admin Role** +- A more specific permission with lower permission level will not have any effect if a more general rule exists with higher permission level. For example if "Everyone with Editor Role Can Edit" exists in the ACL list then **John Doe** will still have Edit permission even after you have specifically added a permission for this user with the permission set to **View**. You need to remove or lower the permission level of the more general rule. diff --git a/docs/sources/guides/whats-new-in-v5.md b/docs/sources/guides/whats-new-in-v5.md index ff73d8aa2b3..87652607827 100644 --- a/docs/sources/guides/whats-new-in-v5.md +++ b/docs/sources/guides/whats-new-in-v5.md @@ -30,7 +30,7 @@ This is the most substantial update that Grafana has ever seen. This article wil ## New Dashboard Layout Engine -{{< docs-imagebox img="/img/docs/v50/new_grid.png" max-width="700px" class="docs-image--right">}} +{{< docs-imagebox img="/img/docs/v50/new_grid.png" max-width="1000px" class="docs-image--right">}} The new dashboard layout engine allows for much easier movement & sizing of panels as other panels now move out of the way in a very intuitive way. No longer do you need to use rows to create layouts as panels are sized independently. This opens @@ -43,7 +43,7 @@ with older versions of Grafana. ## New UX -{{< docs-imagebox img="/img/docs/v50/new_ux_nav.png" max-width="700px" class="docs-image--right" >}} +{{< docs-imagebox img="/img/docs/v50/new_ux_nav.png" max-width="1000px" class="docs-image--right" >}} Almost every page has been seen significant UX improvements. All pages (except dashboard) has new tab-based layout that improves navigation between pages. The side menu has also changed quite a bit. You can still hide the side menu completely if you click on the Grafana logo. @@ -51,7 +51,7 @@ Almost every page has been seen significant UX improvements. All pages (except d ### Dashboard Settings -{{< docs-imagebox img="/img/docs/v50/dashboard_settings.png" max-width="700px" class="docs-image--right" >}} +{{< docs-imagebox img="/img/docs/v50/dashboard_settings.png" max-width="1000px" class="docs-image--right" >}} Dashboard has new header toolbar look where buttons and actions are now all moved to the right. All the dashboard settings views has been combined with a side nav which allows you to easily move between different setting categories. @@ -59,7 +59,7 @@ settings views has been combined with a side nav which allows you to easily move ## New Light Theme -{{< docs-imagebox img="/img/docs/v50/new_white_theme.png" max-width="700px" class="docs-image--right" >}} +{{< docs-imagebox img="/img/docs/v50/new_white_theme.png" max-width="1000px" class="docs-image--right" >}} This theme has not seen a lot of love in recent years and we felt it was time to rework it and give it a major overhaul. We are very happy with the result. @@ -67,7 +67,7 @@ This theme has not seen a lot of love in recent years and we felt it was time to ## Dashboard Folders -{{< docs-imagebox img="/img/docs/v50/new_search.png" max-width="700px" class="docs-image--right" >}} +{{< docs-imagebox img="/img/docs/v50/new_search.png" max-width="1000px" class="docs-image--right" >}} The big new feature that comes with Grafana v5.0 is dashboard folders. Now you can organize your dashboards in folders which is very useful if you have a lot of dashboards or multiple teams. @@ -83,7 +83,7 @@ We hope to do more with teams in future releases like integration with LDAP & a ## Permissions -{{< docs-imagebox img="/img/docs/v50/folder_permissions.png" max-width="700px" class="docs-image--right" >}} +{{< docs-imagebox img="/img/docs/v50/folder_permissions.png" max-width="1000px" class="docs-image--right" >}} You can assign permissions to folders and dashboards. The default user role-based permissions can be removed and replaced with specific teams or users enabling more control over what a user can see & edit. diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 5f458a48aeb..d8c4edbf4d3 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -296,7 +296,7 @@ options are `Admin` and `Editor`. e.g. : `auto_assign_org_role = Viewer` -### viewers can edit +### viewers_can_edit Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard. Defaults to `false`. diff --git a/docs/sources/reference/admin.md b/docs/sources/reference/admin.md deleted file mode 100644 index a6863b4ea71..00000000000 --- a/docs/sources/reference/admin.md +++ /dev/null @@ -1,42 +0,0 @@ -+++ -title = "Admin Roles" -description = "Users & Organization permission and administration" -keywords = ["grafana", "configuration", "documentation", "admin", "users", "permissions"] -type = "docs" -[menu.docs] -name = "Admin Roles" -parent = "admin" -weight = 3 -+++ - -# Administration - -Grafana has two levels of administrators: - -* Organizational administrators: These admins can manage users within specific organizations in a particular Grafana installation -* Grafana administrators: These super admins can manage users across all organizations in a Grafana installation. They can also change and access system-wide settings. - -## Organizational Administrators - -As an Organizational administrator, you can add `Data Sources`, add Users to your Organization and -modify Organization details and options. - -> *Note*: If Grafana is configured with `users.allow_org_create = true`, any User of any Organization will be able to -> start their own Organization and become the administrator of that Organization. - - -## Grafana Administrators - - -As a Grafana Administrator, you have complete access to any Organization or User in that instance of Grafana. -When performing actions as a Grafana admin, the sidebar will change it's appearance as below to indicate you are performing global server administration. - -From the Grafana Server Admin page, you can access the System Info page which summarizes all of the backend configuration settings of the Grafana server. - -## Why would I have multiple Organizations? - -Organizations in Grafana are best suited for a **multi-tenant deployment**. In a multi-tenant deployment, -Organizations can be used to provide a full Grafana experience to different sets of users from a single Grafana instance, -at the convenience of the Grafana Administrator. - -In most cases, a Grafana installation will only have **one** Organization. Since dashboards, data sources and other configuration items are not shared between organizations, there's no need to create multiple Organizations if you want all your users to have access to the same set of dashboards and data. diff --git a/docs/sources/reference/dashboard_folders.md b/docs/sources/reference/dashboard_folders.md index 0ba6d49ed9a..2c287c6891b 100644 --- a/docs/sources/reference/dashboard_folders.md +++ b/docs/sources/reference/dashboard_folders.md @@ -14,20 +14,16 @@ Folders are a way to organize and group dashboards - very useful if you have a l ## How To Create A Folder -- Create a folder by using the Create Folder link in the side menu. - - ![](/img/docs/v50/create_folder_menu.png) - +- Create a folder by using the Create Folder link in the side menu (under the create menu (+ icon)) - Use the create Folder button on the Manage Dashboards page. - -- When saving a dashboard, you can either choose a folder for the dashboard to be saved in or create a new folder (coming in 5.0 beta) +- When saving a dashboard, you can either choose a folder for the dashboard to be saved in or create a new folder On the Create Folder page, fill in a unique name for the folder and press Create. -![](/img/docs/v50/create_folder_page.png) - ## Manage Dashboards +{{< docs-imagebox img="/img/docs/v50/manage_dashboard_menu.png" max-width="300px" class="docs-image--right" >}} + There is a new Manage Dashboards page where you can carry out a variety of tasks: - create a folder @@ -36,62 +32,21 @@ There is a new Manage Dashboards page where you can carry out a variety of tasks - delete multiple dashboards - navigate to a folder page (where you can set permissions for a folder and/or its dashboards) -There is a new option in the Dashboards menu for the Manage Dashboards page: - - ![](/img/docs/v50/manage_dashboard_menu.png) - -Here you can manage your dashboards: - -![](/img/docs/v50/manage_dashboards_page.png) - -Or you can go directly to a Dashboard Folder page via Dashboard Search by clicking on the cog icon: - -![](/img/docs/v50/go_to_dashboard_folder_page.png) - ## Dashboard Folder Page +You reach the dashboard folder page by clicking on the cog icon that appears when you hover +over a folder in the dashboard list in the search result or on the Manage dashboards page. + The Dashboard Folder Page is similar to the Manage Dashboards page and is where you can carry out the following tasks: -- allows you to move or delete dashboards in a folder. -- rename a folder (under the Settings tab). -- set permissions on the whole folder. -- set permissions on a single dashboard. +- Allows you to move or delete dashboards in a folder. +- Rename a folder (under the Settings tab). +- Set permissions for the folder (inherited by dashboards in the folder). -## Dashboard Permissions (Not enabled in Grafana 5.0 alpha) +## Permissions -An Access Control List (ACL) model is used for permissions on Dashboard Folders. An individual user can be assigned permissions on a folder or a Team. +Permissions can assigned to a folder and inherited by the containing dashboards. An Access Control List (ACL) is used where +**Organization Role**, **Team** and Individual **User** can be assigned permissions. Read the + [Dashboard & Folder Permissions]({{< relref "administration/permissions.md#dashboard-folder-permissions" >}}) docs for more detail + on the permission system. -The permissions that can be assigned for a folder are: View, Edit, Admin. - -The default is that: - -- everyone has access to a folder and that their permissions depend on their user role (Viewer, Editor or Admin). -- An Admin or Editor can remove the default access for everyone and can then assign a user or team to a Dashboard Folder. -- Teams make it easier to assign permissions for multiple users to multiple dashboards. - -Other Dashboard Folder rules: - -- Users with the Admin and Editor role are allowed to create new Dashboard Folders. -- Users with the Viewer role are not allowed to create new Dashboard Folders. -- Editors who are owners and Admins can assign permissions to users or teams for Dashboard Folders. -- Default permissions can be removed except for the Admin permissions (View, Edit). - -### Limiting Permissions on a Folder - -To limit permissions on a folder or dashboard: - -1. go to the permissions tab on the Dashboard Folder page -2. remove the default permissions (Everyone with Editor Role / Everyone with Viewer Role) -3. Give a team or user specific permissions. For example: `frontend-team can edit` and `ops-team can view`. - -Remember that users with the Admin role will always have permission to all folders and dashboards. - -## Teams (Not enabled in Grafana 5.0 alpha) - -Teams is a new concept for Grafana 5.0. A team is a group of users that can be assigned permissions on a dashboard folder or a dashboard. - -How Teams Work: - -- Admins can create teams. -- No hierarchies. Teams cannot contain teams. -- If a user belongs to multiple teams, their permissions are merged to give them the highest permission possible for a dashboard folder or dashboard. From e71efbd8730f729be4f0a5931d5ea003284bfb66 Mon Sep 17 00:00:00 2001 From: Trent White Date: Wed, 31 Jan 2018 14:45:09 -0500 Subject: [PATCH 17/58] update text, fix a few typos --- docs/sources/guides/whats-new-in-v5.md | 34 +++++++++++++------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/sources/guides/whats-new-in-v5.md b/docs/sources/guides/whats-new-in-v5.md index 87652607827..89377c5ccf5 100644 --- a/docs/sources/guides/whats-new-in-v5.md +++ b/docs/sources/guides/whats-new-in-v5.md @@ -14,13 +14,13 @@ weight = -6 This is the most substantial update that Grafana has ever seen. This article will detail the major new features and enhancements. -- [New Dashboard Layout Engine]({{< relref "#new-dashboard-layout-engine" >}}) enables a much easier drag, drop and resize experiance and new types of layouts. +- [New Dashboard Layout Engine]({{< relref "#new-dashboard-layout-engine" >}}) enables a much easier drag, drop and resize experience and new types of layouts. - [New UX]({{< relref "#new-ux-layout-engine" >}}). The UI has big improvements in both look and function. - [New Light Theme]({{< relref "#new-light-theme" >}}) is now looking really nice. - [Dashboard Folders]({{< relref "#dashboard-folders" >}}) helps you keep your dashboards organized. -- [Permissions]({{< relref "#dashboard-folders" >}}) on folders and dashboards helps managing a large Grafana installation. +- [Permissions]({{< relref "#dashboard-folders" >}}) on folders and dashboards helps manage larger Grafana installations. - [Group users into teams]({{< relref "#teams" >}}) and use them in the new permission system. -- [Datasource provisioning]({{< relref "#data-sources" >}}) makes it possible to setup data sources via config files. +- [Datasource provisioning]({{< relref "#data-sources" >}}) makes it possible to setup datasources via config files. - [Dashboard provisioning]({{< relref "#dashboards" >}}) makes it possible to setup dashboards via config files. ### Video showing new features @@ -32,8 +32,8 @@ This is the most substantial update that Grafana has ever seen. This article wil {{< docs-imagebox img="/img/docs/v50/new_grid.png" max-width="1000px" class="docs-image--right">}} -The new dashboard layout engine allows for much easier movement & sizing of panels as other panels now move out of the way in -a very intuitive way. No longer do you need to use rows to create layouts as panels are sized independently. This opens +The new dashboard layout engine allows for much easier movement and sizing of panels, as other panels now move out of the way in +a very intuitive way. Panels are sized independently, so rows are no longer necessary to create layouts. This opens up many new types of layouts where panels of different heights can be aligned easily. Checkout the new grid in the video above or on the [play site](http://play.grafana.org). All your existing dashboards will automatically migrate to the new position system and look close to identical. The new panel position makes dashboards saved in v5.0 not compatible @@ -45,15 +45,15 @@ with older versions of Grafana. {{< docs-imagebox img="/img/docs/v50/new_ux_nav.png" max-width="1000px" class="docs-image--right" >}} -Almost every page has been seen significant UX improvements. All pages (except dashboard) has new tab-based layout that improves navigation between pages. The side menu has also changed quite a bit. You can still hide the side menu completely if you click on the Grafana logo. +Almost every page has seen significant UX improvements. All pages (except dashboard pages) have a new tab-based layout that improves navigation between pages. The side menu has also changed quite a bit. You can still hide the side menu completely if you click on the Grafana logo.
### Dashboard Settings {{< docs-imagebox img="/img/docs/v50/dashboard_settings.png" max-width="1000px" class="docs-image--right" >}} -Dashboard has new header toolbar look where buttons and actions are now all moved to the right. All the dashboard -settings views has been combined with a side nav which allows you to easily move between different setting categories. +Dashboard pages have a new header toolbar where buttons and actions are now all moved to the right. All the dashboard +settings views have been combined with a side nav which allows you to easily move between different setting categories.
@@ -69,23 +69,23 @@ This theme has not seen a lot of love in recent years and we felt it was time to {{< docs-imagebox img="/img/docs/v50/new_search.png" max-width="1000px" class="docs-image--right" >}} -The big new feature that comes with Grafana v5.0 is dashboard folders. Now you can organize your dashboards in folders +The big new feature that comes with Grafana v5.0 is dashboard folders. Now you can organize your dashboards in folders, which is very useful if you have a lot of dashboards or multiple teams. -- New search design that adds expandable sections for each folder, starred & recently viewed dashboards. -- New manage dashboard pages that enable batch actions and views for folder settings & permissions. +- New search design adds expandable sections for each folder, starred and recently viewed dashboards. +- New manage dashboard pages enable batch actions and views for folder settings and permissions. - Set permissions on folders and have dashboards inherit the permissions. ## Teams A team is a new concept in Grafana v5. They are simply a group of users that can be then be used in the new permission system for dashboards and folders. Only an admin can create teams. -We hope to do more with teams in future releases like integration with LDAP & a team landing page. +We hope to do more with teams in future releases like integration with LDAP and a team landing page. ## Permissions {{< docs-imagebox img="/img/docs/v50/folder_permissions.png" max-width="1000px" class="docs-image--right" >}} -You can assign permissions to folders and dashboards. The default user role-based permissions can be removed and replaced with specific teams or users enabling more control over what a user can see & edit. +You can assign permissions to folders and dashboards. The default user role-based permissions can be removed and replaced with specific teams or users enabling more control over what a user can see and edit.
@@ -106,7 +106,7 @@ It's also possible to update and delete data sources from the config file. More ### Dashboards We also deprecated the [dashboard.json] in favor of our new dashboard provisioner that keeps dashboards on disk -in sync with dashboards in Grafana's database. The dashboard provisioner have multiple advantages over the old +in sync with dashboards in Grafana's database. The dashboard provisioner has multiple advantages over the old [dashboard.json] feature. Instead of storing the dashboard in memory we now insert the dashboard into the database, which makes it possible to star them, use one as the home dashboard, set permissions and other features in Grafana that expects the dashboards to exist in the database. More info in the [dashboard provisioning docs](/administration/provisioning/#dashboards) @@ -114,7 +114,7 @@ expects the dashboards to exist in the database. More info in the [dashboard pro # Dashboard model & API We are introducing a new identifier (`uid`) in the dashboard JSON model. The new identifier will be a 9-12 character long unique id. -We are also changing the route for getting dashboards to use this `uid` instead of the slug that the current route & API are using. +We are also changing the route for getting dashboards to use this `uid` instead of the slug that the current route and API are using. We will keep supporting the old route for backward compatibility. This will make it possible to change the title on dashboards without breaking links. -Sharing dashboards between instances become much easier since the uid is unique (unique enough). This might seem like a small change, -but we are incredibly excited about it since it will make it much easier to manage, collaborate and navigate between dashboards. \ No newline at end of file +Sharing dashboards between instances becomes much easier since the uid is unique (unique enough). This might seem like a small change, +but we are incredibly excited about it since it will make it much easier to manage, collaborate and navigate between dashboards. From 65bb6cc7287c37b16dc36ee2fe13f641c841ce4a Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 1 Feb 2018 16:53:39 +0100 Subject: [PATCH 18/58] docs: add examples for dashboard permissions --- docs/sources/administration/permissions.md | 44 ++++++++++++++++++---- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/docs/sources/administration/permissions.md b/docs/sources/administration/permissions.md index daecb1807e8..7375d44e6f4 100644 --- a/docs/sources/administration/permissions.md +++ b/docs/sources/administration/permissions.md @@ -55,7 +55,7 @@ This admin flag makes a user a `Super Admin`. This means they can access the `Se {{< docs-imagebox img="/img/docs/v50/folder_permissions.png" max-width="500px" class="docs-image--right" >}} For dashboards and dashboard folders there is a **Permissions** page that make it possible to -remove the default role based permssions for Editors and Viewers. It's here you can add and assign permissions to specific **Users** and **Teams**. +remove the default role based permissions for Editors and Viewers. It is here you can add and assign permissions to specific **Users** and **Teams**. You can assign & remove permissions for **Organization Roles**, **Users** and **Teams**. @@ -63,12 +63,42 @@ Permission levels: - **Admin**: Can edit & create dashboards and edit permissions. - **Edit**: Can edit & create dashboards. **Cannot** edit folder/dashboard permissions. -- **View**: Can only view existing dashboars/folders. +- **View**: Can only view existing dashboards/folders. -#### Restricting access +#### Restricting Access -The highest permission always wins so if you for example want to hide a folder or dashboard from others you need to remove the **Organization Role** based permission from the -Access Control List (ACL). +The highest permission always wins so if you for example want to hide a folder or dashboard from others you need to remove the **Organization Role** based permission from the Access Control List (ACL). -- You cannot override permissions for users with **Org Admin Role** -- A more specific permission with lower permission level will not have any effect if a more general rule exists with higher permission level. For example if "Everyone with Editor Role Can Edit" exists in the ACL list then **John Doe** will still have Edit permission even after you have specifically added a permission for this user with the permission set to **View**. You need to remove or lower the permission level of the more general rule. +- You cannot override permissions for users with the **Org Admin Role**. Admins always have access to everything. +- A more specific permission with a lower permission level will not have any effect if a more general rule exists with higher permission level. You need to remove or lower the permission level of the more general rule. + +#### How Grafana Resolves Multiple Permissions - Examples + +##### Example 1 (`user1` has the Editor Role) + +Permissions for a dashboard: + +- `Everyone with Editor Role Can Edit` +- `user1 Can View` + +Result: `user1` has Edit permission as the highest permission always wins. + +##### Example 2 (`user1` has the Viewer Role and is a member of `team1`) + +Permissions for a dashboard: + +- `Everyone with Viewer Role Can View` +- `user1 Can Edit` +- `team1 Can Admin` + +Result: `user1` has Admin permission as the highest permission always wins. + +##### Example 3 + +Permissions for a dashboard: + +- `user1 Can Admin (inherited from parent folder)` +- `user1 Can Edit` + + +Result: You cannot override to a lower permission. `user1` has Admin permission as the highest permission always wins. From 34ab88298687c3e5bdead6c369391a1e9ce08aef Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 5 Feb 2018 17:12:22 +0100 Subject: [PATCH 19/58] docs: adds http api dashboard permissions --- .../sources/http_api/dashboard_permissions.md | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 docs/sources/http_api/dashboard_permissions.md diff --git a/docs/sources/http_api/dashboard_permissions.md b/docs/sources/http_api/dashboard_permissions.md new file mode 100644 index 00000000000..421c04b1ecb --- /dev/null +++ b/docs/sources/http_api/dashboard_permissions.md @@ -0,0 +1,178 @@ ++++ +title = "Dashboard Permissions HTTP API " +description = "Grafana Dashboard Permissions HTTP API" +keywords = ["grafana", "http", "documentation", "api", "dashboard", "permission", "permissions", "acl"] +aliases = ["/http_api/dashboardpermissions/"] +type = "docs" +[menu.docs] +name = "Dashboard Permissions" +parent = "http_api" ++++ + +# Dashboard Permissions/ACL API + +This API can be used to update/get the ACL for a dashboard or a folder. + +Permissions with dashboardId set to `-1` are the default permissions for users with the Viewer and Editor roles. Permissions can be set for a user, a team or a role (Viewer or Editor). Permissions cannot be set for Admins - they always have access to everything. + +The permission levels for the permission field: + +- 1 = View +- 2 = Edit +- 4 = Admin + +## Get ACL/Permissions for a Dashboard or Folder + +`GET /api/dashboards/id/:dashboardId/acl` + +Gets all existing dashboard permissions for the dashboard or folder with the given `dashboardId`. + +**Example request for getting the ACL/Permissions**: + +```http +GET /api/dashboards/id/1/acl HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +**Example Response** + +```http +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Content-Length: 551 + +[ + { + "id": 1, + "dashboardId": -1, + "created": "2017-06-20T02:00:00+02:00", + "updated": "2017-06-20T02:00:00+02:00", + "userId": 0, + "userLogin": "", + "userEmail": "", + "teamId": 0, + "team": "", + "role": "Viewer", + "permission": 1, + "permissionName": "View", + "uid": "", + "title": "", + "slug": "", + "isFolder": false, + "url": "" + }, + { + "id": 2, + "dashboardId": -1, + "created": "2017-06-20T02:00:00+02:00", + "updated": "2017-06-20T02:00:00+02:00", + "userId": 0, + "userLogin": "", + "userEmail": "", + "teamId": 0, + "team": "", + "role": "Editor", + "permission": 2, + "permissionName": "Edit", + "uid": "", + "title": "", + "slug": "", + "isFolder": false, + "url": "" + } +] +``` + +Status Codes: + +- **200** - Ok +- **403** - Access denied +- **404** - Dashboard not found + +## Save Dashboard Permissions/ACL for a Dashboard or Folder + +`POST /api/dashboards/id/:dashboardId/acl` + +Updates the ACL for a dashboard or folder. Takes in a list of permissions and adds, remove or updates permissions in the list in the database. + +**Example request for saving a list of permission items**: + +```http +POST /api/dashboards/id/1/acl +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + + "items": [ + { + "role": "Viewer", + "permission": 1 + }, + { + "role": "Editor", + "permission": 2 + }, + { + "teamId": 1, + "permission": 1 + }, + { + "userId": 11, + "permission": 4 + } + ] +} +``` + +JSON body schema: + +- **items** - The permission items to add/update. Items that are omitted from the list will be removed from the db. + +**Example response**: + +```http +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Content-Length: 35 + +{"message":"Dashboard acl updated"} +``` + +Status Codes: + +- **200** - Ok +- **403** - Access denied +- **404** - Dashboard not found + + +## Delete Permission from the Dashboard ACL/Permissions List for a Dashboard or Folder + +`DELETE /api/dashboards/id/:dashboardId/acl/:aclId` + +The above will delete an item from a dashboard or folder ACL given the id of the permission item. + +**Example Request**: + +```http +DELETE /api/dashboards/id/1/acl/540 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json +Content-Length: 0 +``` + +Status Codes: + +- **200** - Ok +- **403** - Access denied +- **404** - Dashboard permission not found + From 1e0817f8634e14702b9f70371316f949d209abf0 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 5 Feb 2018 20:53:58 +0100 Subject: [PATCH 20/58] docs: update http api for api index, dashboard, folder and dashboard search --- docs/sources/http_api/dashboard.md | 205 ++++++++++++++---- .../http_api/folder_dashboard_search.md | 98 +++++++++ docs/sources/http_api/index.md | 1 + 3 files changed, 256 insertions(+), 48 deletions(-) create mode 100644 docs/sources/http_api/folder_dashboard_search.md diff --git a/docs/sources/http_api/dashboard.md b/docs/sources/http_api/dashboard.md index 0538754bd96..86c2d0e015f 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 ``, `status=plugin-dashboard` + + The response body will have the following properties: ```http HTTP/1.1 412 Precondition Failed @@ -85,16 +112,16 @@ Content-Length: 97 In in case of title already exists the `status` property will be `name-exists`. -## Get dashboard +## Get dashboard by uid -`GET /api/dashboards/db/:slug` +`GET /api/dashboards/uid/:uid` -Will return the dashboard given the dashboard slug. Slug is the url friendly version of the dashboard title. +Will return the dashboard given the dashboard unique identifier (uid). **Example Request**: ```http -GET /api/dashboards/db/production-overview HTTP/1.1 +GET /api/dashboards/uid/cIBgcSjkk HTTP/1.1 Accept: application/json Content-Type: application/json Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk @@ -107,12 +134,9 @@ HTTP/1.1 200 Content-Type: application/json { - "meta": { - "isStarred": false, - "slug": "production-overview" - }, "dashboard": { - "id": null, + "id": 1, + "uid": "cIBgcSjkk", "title": "Production Overview", "tags": [ "templated" ], "timezone": "browser", @@ -122,20 +146,32 @@ Content-Type: application/json ], "schemaVersion": 6, "version": 0 + }, + "meta": { + "isStarred": false, + "url": "/d/cIBgcSjkk/production-overview", + "slug": "production-overview" //deprecated in Grafana v5.0 } } ``` -## Delete dashboard +Status Codes: -`DELETE /api/dashboards/db/:slug` +- **200** – Found +- **401** – Unauthorized +- **403** – Access denied +- **404** – Not found -The above will delete the dashboard with the specified slug. The slug is the url friendly (unique) version of the dashboard title. +## Delete dashboard by uid + +`DELETE /api/dashboards/uid/:uid` + +Will delete the dashboard given the specified unique identifier (uid). **Example Request**: ```http -DELETE /api/dashboards/db/test HTTP/1.1 +DELETE /api/dashboards/uid/cIBgcSjkk HTTP/1.1 Accept: application/json Content-Type: application/json Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk @@ -147,9 +183,16 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk HTTP/1.1 200 Content-Type: application/json -{"title": "Test"} +{"title": "Production Overview"} ``` +Status Codes: + +- **200** – Deleted +- **401** – Unauthorized +- **403** – Access denied +- **404** – Not found + ## Gets the home dashboard `GET /api/dashboards/home` @@ -172,15 +215,6 @@ HTTP/1.1 200 Content-Type: application/json { - "meta": { - "isHome":true, - "canSave":false, - "canEdit":false, - "canStar":false, - "slug":"", - "expires":"0001-01-01T00:00:00Z", - "created":"0001-01-01T00:00:00Z" - }, "dashboard": { "editable":false, "hideControls":true, @@ -206,13 +240,21 @@ Content-Type: application/json "timezone":"browser", "title":"Home", "version":5 + }, + "meta": { + "isHome":true, + "canSave":false, + "canEdit":false, + "canStar":false, + "url":"", + "expires":"0001-01-01T00:00:00Z", + "created":"0001-01-01T00:00:00Z" } } ``` ## Tags for Dashboard - `GET /api/dashboards/tags` Get all tags of dashboards @@ -244,21 +286,24 @@ Content-Type: application/json ] ``` -## Search Dashboards +## Dashboard Search +See [Folder/Dashboard Search API](/http_api/folder_dashboard_search). -`GET /api/search/` +## Deprecated resources +Please note that these resource have been deprecated and will be removed in a future release. -Query parameters: +### Get dashboard by slug +**Deprecated starting from Grafana v5.0. Please update to use the new *Get dashboard by uid* resource instead** -- **query** – Search Query -- **tag** – Tag to use -- **starred** – Flag indicating if only starred Dashboards should be returned -- **tagcloud** - Flag indicating if a tagcloud should be returned +`GET /api/dashboards/db/:slug` + +Will return the dashboard given the dashboard slug. Slug is the url friendly version of the dashboard title. +If there exists multiple dashboards with the same slug, one of them will be returned in the response. **Example Request**: ```http -GET /api/search?query=Production%20Overview&starred=true&tag=prod HTTP/1.1 +GET /api/dashboards/db/production-overview HTTP/1.1 Accept: application/json Content-Type: application/json Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk @@ -270,14 +315,78 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk HTTP/1.1 200 Content-Type: application/json -[ - { - "id":1, - "title":"Production Overview", - "uri":"db/production-overview", - "type":"dash-db", - "tags":[prod], - "isStarred":true +{ + "dashboard": { + "id": 1, + "uid": "cIBgcSjkk", + "title": "Production Overview", + "tags": [ "templated" ], + "timezone": "browser", + "rows": [ + { + } + ], + "schemaVersion": 6, + "version": 0 + }, + "meta": { + "isStarred": false, + "url": "/d/cIBgcSjkk/production-overview", + "slug": "production-overview" // deprecated in Grafana v5.0 } -] +} +``` + +Status Codes: + +- **200** – Found +- **401** – Unauthorized +- **403** – Access denied +- **404** – Not found + +### Delete dashboard by slug +**Deprecated starting from Grafana v5.0. Please update to use the *Delete dashboard by uid* resource instead.** + +`DELETE /api/dashboards/db/:slug` + +Will delete the dashboard given the specified slug. Slug is the url friendly version of the dashboard title. + +**Example Request**: + +```http +DELETE /api/dashboards/db/test HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +{"title": "Production Overview"} +``` + +Status Codes: + +- **200** – Deleted +- **401** – Unauthorized +- **403** – Access denied +- **404** – Not found +- **412** – Precondition failed + +The **412** status code is used when there exists multiple dashboards with the same slug. +The response body will look like this: + +```http +HTTP/1.1 412 Precondition Failed +Content-Type: application/json; charset=UTF-8 +Content-Length: 97 + +{ + "message": "Multiple dashboards with the same slug exists", + "status": "multiple-slugs-exists" +} ``` diff --git a/docs/sources/http_api/folder_dashboard_search.md b/docs/sources/http_api/folder_dashboard_search.md new file mode 100644 index 00000000000..73b5dd90b87 --- /dev/null +++ b/docs/sources/http_api/folder_dashboard_search.md @@ -0,0 +1,98 @@ ++++ +title = "Folder/Dashboard Search HTTP API " +description = "Grafana Folder/Dashboard Search HTTP API" +keywords = ["grafana", "http", "documentation", "api", "search", "folder", "dashboard"] +aliases = ["/http_api/folder_dashboard_search/"] +type = "docs" +[menu.docs] +name = "Folder/dashboard search" +parent = "http_api" ++++ + +# Folder/Dashboard Search API + +## Search folders and dashboards + +`GET /api/search/` + +Query parameters: + +- **query** – Search Query +- **tag** – List of tags to search for +- **type** – Type to search for, `dash-folder` or `dash-db` +- **dashboardIds** – List of dashboard id's to search for +- **folderIds** – List of folder id's to search in for dashboards +- **starred** – Flag indicating if only starred Dashboards should be returned +- **limit** – Limit the number of returned results + +**Example request for retrieving folders and dashboards of the general folder**: + +```http +GET /api/search?folderIds=0&query=&starred=false HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +**Example response for retrieving folders and dashboards of the general folder**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +[ + { + "id": 163, + "uid": "000000163", + "title": "Folder", + "url": "/dashboards/f/000000163/folder", + "type": "dash-folder", + "tags": [], + "isStarred": false, + "uri":"db/folder" // deprecated in Grafana v5.0 + }, + { + "id":1, + "uid": "cIBgcSjkk", + "title":"Production Overview", + "url": "/d/cIBgcSjkk/production-overview", + "type":"dash-db", + "tags":[prod], + "isStarred":true, + "uri":"db/production-overview" // deprecated in Grafana v5.0 + } +] +``` + +**Example request searching for dashboards**: + +```http +GET /api/search?query=Production%20Overview&starred=true&tag=prod HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +**Example response searching for dashboards**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +[ + { + "id":1, + "uid": "cIBgcSjkk", + "title":"Production Overview", + "url": "/d/cIBgcSjkk/production-overview", + "type":"dash-db", + "tags":[prod], + "isStarred":true, + "folderId": 2, + "folderUid": "000000163", + "folderTitle": "Folder", + "folderUrl": "/dashboards/f/000000163/folder", + "uri":"db/production-overview" // deprecated in Grafana v5.0 + } +] +``` \ No newline at end of file diff --git a/docs/sources/http_api/index.md b/docs/sources/http_api/index.md index cbfe004b14c..b00deaf6715 100644 --- a/docs/sources/http_api/index.md +++ b/docs/sources/http_api/index.md @@ -21,6 +21,7 @@ dashboards, creating users and updating data sources. * [Authentication API]({{< relref "/http_api/auth.md" >}}) * [Dashboard API]({{< relref "/http_api/dashboard.md" >}}) * [Dashboard Versions API]({{< relref "http_api/dashboard_versions.md" >}}) +* [Folder/dashboard search API]({{< relref "/http_api/folder_dashboard_search.md" >}}) * [Data Source API]({{< relref "http_api/data_source.md" >}}) * [Organisation API]({{< relref "http_api/org.md" >}}) * [Snapshot API]({{< relref "http_api/snapshot.md" >}}) From 76dc7932a405e7762fac33e01a60dbd9eb838379 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 2 Feb 2018 16:14:48 +0100 Subject: [PATCH 21/58] docs: describe uid for dashboard provisioning --- conf/provisioning/dashboards/sample.yaml | 2 +- docs/sources/administration/provisioning.md | 8 ++++++-- .../dashboards/test-configs/broken-configs/commented.yaml | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/conf/provisioning/dashboards/sample.yaml b/conf/provisioning/dashboards/sample.yaml index 40992d1461e..f0dcca9b47a 100644 --- a/conf/provisioning/dashboards/sample.yaml +++ b/conf/provisioning/dashboards/sample.yaml @@ -3,4 +3,4 @@ # folder: '' # type: file # options: -# folder: /var/lib/grafana/dashboards \ No newline at end of file +# path: /var/lib/grafana/dashboards diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index c3595969281..b499efba1e1 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -179,7 +179,11 @@ The dashboard provider config file looks somewhat like this: folder: '' type: file 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 path. Then later on poll that path and look for updated json files and insert those update/insert those into the database. + +### Reuseable dashboard urls + +If the dashboard in the json file contains an [uid](http://localhost:3004/reference/dashboard/#json-fields), Grafana will force insert/update on that uid. This allows you to migrate dashboards betweens Grafana instances and provisioning Grafana from configuration without breaking the urls given since the new dashboard url uses the uid as identifer. 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 From 4eec4bfb0534b7a0cfd2babbf262dffd9474d069 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 8 Feb 2018 09:55:12 +0100 Subject: [PATCH 22/58] docs: adds uid to dashboard.json reference docs --- docs/sources/reference/dashboard.md | 54 +++-------------------------- 1 file changed, 5 insertions(+), 49 deletions(-) diff --git a/docs/sources/reference/dashboard.md b/docs/sources/reference/dashboard.md index 13f08a1ddaf..430d3ce7aeb 100644 --- a/docs/sources/reference/dashboard.md +++ b/docs/sources/reference/dashboard.md @@ -10,7 +10,7 @@ weight = 100 # Dashboard JSON -A dashboard in Grafana is represented by a JSON object, which stores metadata of its dashboard. Dashboard metadata includes dashboard properties, metadata from rows, panels, template variables, panel queries, etc. +A dashboard in Grafana is represented by a JSON object, which stores metadata of its dashboard. Dashboard metadata includes dashboard properties, metadata from panels, template variables, panel queries, etc. To view the JSON of a dashboard, follow the steps mentioned below: @@ -27,6 +27,7 @@ When a user creates a new dashboard, a new dashboard JSON object is initialized ```json { "id": null, + "uid": "cLV5GDCkz", "title": "New dashboard", "tags": [], "style": "dark", @@ -34,7 +35,7 @@ When a user creates a new dashboard, a new dashboard JSON object is initialized "editable": true, "hideControls": false, "graphTooltip": 1, - "rows": [], + "panels": [], "time": { "from": "now-6h", "to": "now" @@ -58,7 +59,8 @@ Each field in the dashboard JSON is explained below with its usage: | Name | Usage | | ---- | ----- | -| **id** | unique dashboard id, an integer | +| **id** | unique numeric identifier for the dashboard. (generated by the db) | +| **uid** | unique dashboard identifier that can be generated by anyone. string (8-40) | | **title** | current title of dashboard | | **tags** | tags associated with dashboard, an array of strings | | **style** | theme of dashboard, i.e. `dark` or `light` | @@ -66,7 +68,6 @@ Each field in the dashboard JSON is explained below with its usage: | **editable** | whether a dashboard is editable or not | | **hideControls** | whether row controls on the left in green are hidden or not | | **graphTooltip** | 0 for no shared crosshair or tooltip (default), 1 for shared crosshair, 2 for shared crosshair AND shared tooltip | -| **rows** | row metadata, see [rows section](#rows) for details | | **time** | time range for dashboard, i.e. last 6 hours, last 7 days, etc | | **timepicker** | timepicker metadata, see [timepicker section](#timepicker) for details | | **templating** | templating metadata, see [templating section](#templating) for details | @@ -75,39 +76,6 @@ Each field in the dashboard JSON is explained below with its usage: | **version** | version of the dashboard (integer), incremented each time the dashboard is updated | | **links** | TODO | -### rows - -`rows` field consists of an array of JSON object representing each row in a dashboard, such as shown below: - -```json - "rows": [ - { - "collapse": false, - "editable": true, - "height": "200px", - "panels": [], - "title": "New row" - }, - { - "collapse": true, - "editable": true, - "height": "300px", - "panels": [], - "title": "New row" - } - ] -``` - -Usage of the fields is explained below: - -| Name | Usage | -| ---- | ----- | -| **collapse** | whether row is collapsed or not | -| **editable** | whether a row is editable or not | -| **height** | height of the row in pixels | -| **panels** | panels metadata, see [panels section](#panels) for details | -| **title** | title of row | - #### panels Panels are the building blocks a dashboard. It consists of datasource queries, type of graphs, aliases, etc. Panel JSON consists of an array of JSON objects, each representing a different panel in a row. Most of the fields are common for all panels but some fields depends on the panel type. Following is an example of panel JSON representing a `graph` panel type: @@ -266,16 +234,8 @@ Panels are the building blocks a dashboard. It consists of datasource queries, t }, ``` -Usage of each field is explained below: - -| Name | Usage | -| ---- | ----- | -| TODO | TODO | - ### timepicker -Description: TODO - ```json "timepicker": { "collapse": false, @@ -416,7 +376,3 @@ Usage of the above mentioned fields in the templating section is explained below | **refresh** | TODO | | **regex** | TODO | | **type** | type of variable, i.e. `custom`, `query` or `interval` | - -### annotations - -TODO From 2bddf501843b8d93abb66282439e16c7cb366f78 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 12 Feb 2018 14:50:35 +0100 Subject: [PATCH 23/58] docs: update dashboard permissions http api docs --- .../sources/http_api/dashboard_permissions.md | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/docs/sources/http_api/dashboard_permissions.md b/docs/sources/http_api/dashboard_permissions.md index 421c04b1ecb..3d54b4011d4 100644 --- a/docs/sources/http_api/dashboard_permissions.md +++ b/docs/sources/http_api/dashboard_permissions.md @@ -9,11 +9,11 @@ name = "Dashboard Permissions" parent = "http_api" +++ -# Dashboard Permissions/ACL API +# Dashboard Permissions API -This API can be used to update/get the ACL for a dashboard or a folder. +This API can be used to update/get the permissions for a dashboard or a folder. -Permissions with dashboardId set to `-1` are the default permissions for users with the Viewer and Editor roles. Permissions can be set for a user, a team or a role (Viewer or Editor). Permissions cannot be set for Admins - they always have access to everything. +Permissions with `dashboardId=-1` are the default permissions for users with the Viewer and Editor roles. Permissions can be set for a user, a team or a role (Viewer or Editor). Permissions cannot be set for Admins - they always have access to everything. The permission levels for the permission field: @@ -21,13 +21,13 @@ The permission levels for the permission field: - 2 = Edit - 4 = Admin -## Get ACL/Permissions for a Dashboard or Folder +## Get permissions for a dashboard or folder `GET /api/dashboards/id/:dashboardId/acl` -Gets all existing dashboard permissions for the dashboard or folder with the given `dashboardId`. +Gets all existing permissions for the dashboard or folder with the given `dashboardId`. -**Example request for getting the ACL/Permissions**: +**Example request**: ```http GET /api/dashboards/id/1/acl HTTP/1.1 @@ -88,16 +88,17 @@ Content-Length: 551 Status Codes: - **200** - Ok +- **401** - Unauthorized - **403** - Access denied -- **404** - Dashboard not found +- **404** - Dashboard/folder not found -## Save Dashboard Permissions/ACL for a Dashboard or Folder +## Update permissions for a dashboard or folder `POST /api/dashboards/id/:dashboardId/acl` -Updates the ACL for a dashboard or folder. Takes in a list of permissions and adds, remove or updates permissions in the list in the database. +Updates permissions for a dashboard or folder. This operation will remove existing permissions if they're not included in the request. -**Example request for saving a list of permission items**: +**Example request**: ```http POST /api/dashboards/id/1/acl @@ -128,7 +129,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk JSON body schema: -- **items** - The permission items to add/update. Items that are omitted from the list will be removed from the db. +- **items** - The permission items to add/update. Items that are omitted from the list will be removed. **Example response**: @@ -143,15 +144,16 @@ Content-Length: 35 Status Codes: - **200** - Ok +- **401** - Unauthorized - **403** - Access denied - **404** - Dashboard not found -## Delete Permission from the Dashboard ACL/Permissions List for a Dashboard or Folder +## Delete permission for a dashboard or folder `DELETE /api/dashboards/id/:dashboardId/acl/:aclId` -The above will delete an item from a dashboard or folder ACL given the id of the permission item. +The above will delete a permission from a dashboard or folder given the permission id. **Example Request**: @@ -173,6 +175,7 @@ Content-Length: 0 Status Codes: - **200** - Ok +- **401** - Unauthorized - **403** - Access denied - **404** - Dashboard permission not found From 5b318428c4d9cd1e09ec96e1dfafe948da99cd0b Mon Sep 17 00:00:00 2001 From: stukselbax Date: Wed, 14 Feb 2018 11:34:46 +0300 Subject: [PATCH 24/58] Invalid url in docs --- docs/sources/administration/provisioning.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index b499efba1e1..1563625b85c 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -186,4 +186,4 @@ When Grafana starts, it will update/insert all dashboards available in the confi ### Reuseable dashboard urls -If the dashboard in the json file contains an [uid](http://localhost:3004/reference/dashboard/#json-fields), Grafana will force insert/update on that uid. This allows you to migrate dashboards betweens Grafana instances and provisioning Grafana from configuration without breaking the urls given since the new dashboard url uses the uid as identifer. +If the dashboard in the json file contains an [uid](/reference/dashboard/#json-fields), Grafana will force insert/update on that uid. This allows you to migrate dashboards betweens Grafana instances and provisioning Grafana from configuration without breaking the urls given since the new dashboard url uses the uid as identifer. From 8377d3630c4c85e3dac0aaec94ab7fb9ebd752ae Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 15 Feb 2018 23:05:16 +0100 Subject: [PATCH 25/58] docs: team API. Closes #10832 --- .../sources/http_api/dashboard_permissions.md | 32 -- docs/sources/http_api/team.md | 306 ++++++++++++++++++ 2 files changed, 306 insertions(+), 32 deletions(-) create mode 100644 docs/sources/http_api/team.md diff --git a/docs/sources/http_api/dashboard_permissions.md b/docs/sources/http_api/dashboard_permissions.md index 3d54b4011d4..4b87dbf83c2 100644 --- a/docs/sources/http_api/dashboard_permissions.md +++ b/docs/sources/http_api/dashboard_permissions.md @@ -147,35 +147,3 @@ Status Codes: - **401** - Unauthorized - **403** - Access denied - **404** - Dashboard not found - - -## Delete permission for a dashboard or folder - -`DELETE /api/dashboards/id/:dashboardId/acl/:aclId` - -The above will delete a permission from a dashboard or folder given the permission id. - -**Example Request**: - -```http -DELETE /api/dashboards/id/1/acl/540 HTTP/1.1 -Accept: application/json -Content-Type: application/json -Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk -``` - -**Example Response**: - -```http -HTTP/1.1 200 -Content-Type: application/json -Content-Length: 0 -``` - -Status Codes: - -- **200** - Ok -- **401** - Unauthorized -- **403** - Access denied -- **404** - Dashboard permission not found - diff --git a/docs/sources/http_api/team.md b/docs/sources/http_api/team.md new file mode 100644 index 00000000000..1aed5fec395 --- /dev/null +++ b/docs/sources/http_api/team.md @@ -0,0 +1,306 @@ ++++ +title = "Team HTTP API " +description = "Grafana Team HTTP API" +keywords = ["grafana", "http", "documentation", "api", "team", "teams", "group"] +aliases = ["/http_api/team/"] +type = "docs" +[menu.docs] +name = "Teams" +parent = "http_api" ++++ + +# Team API + +This API can be used to create/update/delete Teams and to add/remove users to Teams. All actions require that the user has the Admin role for the organization. + +## Team Search With Paging + +`GET /api/teams/search?perpage=50&page=1&query=mytea` + +or + +`GET /api/teams/search?name=myteam` + +```http +GET /api/teams/search?perpage=10&page=1&query=myteam HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Basic YWRtaW46YWRtaW4= +``` + +### Using the query parameter + +Default value for the `perpage` parameter is `1000` and for the `page` parameter is `1`. + +The `totalCount` field in the response can be used for pagination of the teams list E.g. if `totalCount` is equal to 100 teams and the `perpage` parameter is set to 10 then there are 10 pages of teams. + +The `query` parameter is optional and it will return results where the query value is contained in the `name` field. Query values with spaces need to be url encoded e.g. `query=my%20team`. + +### Using the name parameter + +The `name` parameter returns a single team if the parameter matches the `name` field. + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + + "totalCount": 1, + "teams": [ + { + "id": 1, + "orgId": 1, + "name": "MyTestTeam", + "email": "", + "avatarUrl": "\/avatar\/3f49c15916554246daa714b9bd0ee398", + "memberCount": 1 + } + ], + "page": 1, + "perPage": 1000 +} +``` + +Status Codes: + +- **200** - Ok +- **401** - Unauthorized +- **403** - Permission denied + +## Get Team By Id + +`GET /api/teams/:id` + +**Example Request**: + +```http +GET /api/teams/1 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Basic YWRtaW46YWRtaW4= +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +{ + "id": 1, + "orgId": 1, + "name": "MyTestTeam", + "email": "", + "created": "2017-12-15T10:40:45+01:00", + "updated": "2017-12-15T10:40:45+01:00" +} +``` + +Status Codes: + +- **200** - Ok +- **401** - Unauthorized +- **403** - Permission denied +- **404** - Team not found + +## Add Team + +The Team `name` needs to be unique. `name` is required and `email` is optional. + +`POST /api/teams` + +**Example Request**: + +```http +POST /api/teams HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Basic YWRtaW46YWRtaW4= + +{ + "name": "MyTestTeam", + "email": "email@test.com" +} +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +{"message":"Team created","teamId":2} +``` + +Status Codes: + +- **200** - Ok +- **401** - Unauthorized +- **403** - Permission denied +- **409** - Team name is taken + +## Update Team + +There are two fields that can be updated for a team: `name` and `email`. + +`PUT /api/teams/:id` + +**Example Request**: + +```http +PUT /api/teams/2 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Basic YWRtaW46YWRtaW4= + +{ + "name": "MyTestTeam", + "email": "email@test.com" +} +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +{"message":"Team updated"} +``` + +## Delete Team By Id + +`DELETE /api/teams/:id` + +**Example Request**: + +```http +DELETE /api/teams/2 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Basic YWRtaW46YWRtaW4= +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +{"message":"Team deleted"} +``` + +Status Codes: + +- **200** - Ok +- **401** - Unauthorized +- **403** - Permission denied +- **404** - Failed to delete Team. ID not found + +## Get Team Members + +`GET /api/teams/:teamId/members` + +**Example Request**: + +```http +GET /api/teams/1/members HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Basic YWRtaW46YWRtaW4= +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +[ + { + "orgId": 1, + "teamId": 1, + "userId": 3, + "email": "user1@email.com", + "login": "user1", + "avatarUrl": "\/avatar\/1b3c32f6386b0185c40d359cdc733a79" + }, + { + "orgId": 1, + "teamId": 1, + "userId": 2, + "email": "user2@email.com", + "login": "user2", + "avatarUrl": "\/avatar\/cad3c68da76e45d10269e8ef02f8e73e" + } +] +``` + +Status Codes: + +- **200** - Ok +- **401** - Unauthorized +- **403** - Permission denied + +## Add Team Member + +`POST /api/teams/:teamId/members` + +**Example Request**: + +```http +POST /api/teams/1/members HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Basic YWRtaW46YWRtaW4= + +{ + "userId": 2 +} +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +{"message":"Member added to Team"} +``` + +Status Codes: + +- **200** - Ok +- **400** - User is already added to this team +- **401** - Unauthorized +- **403** - Permission denied +- **500** - Failed to add Member to Team + +## Remove Member From Team + +`DELETE /api/teams/:teamId/members/:userId` + +**Example Request**: + +```http +DELETE /api/teams/2/members/3 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Basic YWRtaW46YWRtaW4= +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +{"message":"Team Member removed"} +``` + +Status Codes: + +- **200** - Ok +- **401** - Unauthorized +- **403** - Permission denied From 2bcd7cd25b89ce6e41af99cc163de0e24670cd97 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Fri, 16 Feb 2018 11:18:41 +0100 Subject: [PATCH 26/58] docs: status code changes for Team API --- docs/sources/http_api/team.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/sources/http_api/team.md b/docs/sources/http_api/team.md index 1aed5fec395..94ea4108481 100644 --- a/docs/sources/http_api/team.md +++ b/docs/sources/http_api/team.md @@ -67,6 +67,7 @@ Status Codes: - **200** - Ok - **401** - Unauthorized - **403** - Permission denied +- **404** - Team not found (if searching by name) ## Get Team By Id @@ -169,6 +170,14 @@ Content-Type: application/json {"message":"Team updated"} ``` +Status Codes: + +- **200** - Ok +- **401** - Unauthorized +- **403** - Permission denied +- **404** - Team not found +- **409** - Team name is taken + ## Delete Team By Id `DELETE /api/teams/:id` @@ -275,7 +284,7 @@ Status Codes: - **400** - User is already added to this team - **401** - Unauthorized - **403** - Permission denied -- **500** - Failed to add Member to Team +- **404** - Team not found ## Remove Member From Team @@ -304,3 +313,4 @@ Status Codes: - **200** - Ok - **401** - Unauthorized - **403** - Permission denied +- **404** - Team not found/Team member not found From b5e96f5ec2d14abeab6d5839d0ca6ab94ea73700 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 21 Feb 2018 13:09:01 +0100 Subject: [PATCH 27/58] docs: folder http api --- docs/sources/http_api/folder.md | 337 ++++++++++++++++++++++++++++++++ 1 file changed, 337 insertions(+) create mode 100644 docs/sources/http_api/folder.md diff --git a/docs/sources/http_api/folder.md b/docs/sources/http_api/folder.md new file mode 100644 index 00000000000..13de0aab6c2 --- /dev/null +++ b/docs/sources/http_api/folder.md @@ -0,0 +1,337 @@ ++++ +title = "Folder HTTP API " +description = "Grafana Folder HTTP API" +keywords = ["grafana", "http", "documentation", "api", "folder"] +aliases = ["/http_api/folder/"] +type = "docs" +[menu.docs] +name = "Folder" +parent = "http_api" ++++ + +# Folder API + +## Identifier (id) vs unique identifier (uid) + +The identifier (id) of a folder is an auto-incrementing numeric value and is only unique per Grafana install. + +The unique identifier (uid) of a folder can be used for uniquely identify folders between multiple Grafana installs. It's automatically generated if not provided when creating a folder. The uid allows having consistent URL's for accessing folders and when syncing folders between multiple Grafana installs. This means that changing the title of a folder will not break any bookmarked links to that folder. + +The uid can have a maximum length of 40 characters. + + +## Get all folders + +`GET /api/folders` + +Returns all folders that the authenticated user has permission to view. + +**Example Request**: + +```http +GET /api/folders?limit=10 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +[ + { + "id":1, + "uid": "nErXDvCkzz", + "title": "Departmenet ABC", + "url": "/dashboards/f/nErXDvCkzz/department-abc", + "hasAcl": false, + "canSave": true, + "canEdit": true, + "canAdmin": true, + "createdBy": "admin", + "created": "2018-01-31T17:43:12+01:00", + "updatedBy": "admin", + "updated": "2018-01-31T17:43:12+01:00", + "version": 1 + } +] +``` + +## Get folder by uid + +`GET /api/folders/:uid` + +Will return the folder given the folder uid. + +**Example Request**: + +```http +GET /api/folders/nErXDvCkzzh HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +{ + "id":1, + "uid": "nErXDvCkzz", + "title": "Departmenet ABC", + "url": "/dashboards/f/nErXDvCkzz/department-abc", + "hasAcl": false, + "canSave": true, + "canEdit": true, + "canAdmin": true, + "createdBy": "admin", + "created": "2018-01-31T17:43:12+01:00", + "updatedBy": "admin", + "updated": "2018-01-31T17:43:12+01:00", + "version": 1 +} +``` + +Status Codes: + +- **200** – Found +- **401** – Unauthorized +- **403** – Access Denied +- **404** – Folder not found + +## Create folder + +`POST /api/folders` + +Creates a new folder. + +**Example Request**: + +```http +POST /api/folders HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +{ + "uid": "nErXDvCkzz", + "title": "Department ABC" +} +``` + +JSON Body schema: + +- **uid** – Optional [unique identifier](/http_api/folder/#identifier-id-vs-unique-identifier-uid). +- **title** – The title of the folder. + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +{ + "id":1, + "uid": "nErXDvCkzz", + "title": "Departmenet ABC", + "url": "/dashboards/f/nErXDvCkzz/department-abc", + "hasAcl": false, + "canSave": true, + "canEdit": true, + "canAdmin": true, + "createdBy": "admin", + "created": "2018-01-31T17:43:12+01:00", + "updatedBy": "admin", + "updated": "2018-01-31T17:43:12+01:00", + "version": 1 +} +``` + +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 for explaing that you cannot create the folder and why. +There can be different reasons for this: + +- A folder or dashboard in the general folder with the same name already exists, `status=name-exists` +- A folder/dashboard with the same uid already exists, `status=uid-exists` + + The response body will have the following properties: + +```http +HTTP/1.1 412 Precondition Failed +Content-Type: application/json; charset=UTF-8 +Content-Length: 97 + +{ + "message": "A folder or dashboard in the general folder with the same name already exists", + "status": "name-exists" +} +``` + +## Update folder + +`PUT /api/folders/:uid` + +Updates an existing folder identified by uid. + +**Example Request**: + +```http +PUT /api/folders/nErXDvCkzz HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +{ + "title":"Department DEF", + "version": 1 +} +``` + +JSON Body schema: + +- **title** – The title of the folder. +- **version** – Provide the current version to be able to update the folder. Not needed if `overwrite=true`. +- **overwrite** – Set to true if you want to overwrite existing folder with newer version. + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +{ + "id":1, + "uid": "nErXDvCkzz", + "title": "Departmenet DEF", + "url": "/dashboards/f/nErXDvCkzz/department-def", + "hasAcl": false, + "canSave": true, + "canEdit": true, + "canAdmin": true, + "createdBy": "admin", + "created": "2018-01-31T17:43:12+01:00", + "updatedBy": "admin", + "updated": "2018-01-31T17:43:12+01:00", + "version": 1 +} +``` + +Status Codes: + +- **200** – Updated +- **400** – Errors (invalid json, missing or invalid fields, etc) +- **401** – Unauthorized +- **403** – Access Denied +- **404** – Folder not found +- **412** – Precondition failed + +The **412** status code is used for explaing that you cannot update the folder and why. +There can be different reasons for this: + +- A folder or dashboard in the general folder with the same name already exists, `status=name-exists` +- The folder has been changed by someone else, `status=version-mismatch` + + The response body will have the following properties: + +```http +HTTP/1.1 412 Precondition Failed +Content-Type: application/json; charset=UTF-8 +Content-Length: 97 + +{ + "message": "The folder has been changed by someone else", + "status": "version-mismatch" +} +``` + +## Delete folder + +`DELETE /api/folders/:uid` + +Deletes an existing folder identified by uid together with all dashboards stored in the folder, if any. This operation cannot be reverted. + +**Example Request**: + +```http +DELETE /api/folders/nErXDvCkzz HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +{ + "message":"Folder deleted" +} +``` + +Status Codes: + +- **200** – Deleted +- **401** – Unauthorized +- **403** – Access Denied +- **404** – Folder not found + +## Get folder by id + +`GET /api/folders/:id` + +Will return the folder identified by id. + +**Example Request**: + +```http +GET /api/folders/1 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +{ + "id":1, + "uid": "nErXDvCkzz", + "title": "Departmenet ABC", + "url": "/dashboards/f/nErXDvCkzz/department-abc", + "hasAcl": false, + "canSave": true, + "canEdit": true, + "canAdmin": true, + "createdBy": "admin", + "created": "2018-01-31T17:43:12+01:00", + "updatedBy": "admin", + "updated": "2018-01-31T17:43:12+01:00", + "version": 1 +} +``` + +Status Codes: + +- **200** – Found +- **401** – Unauthorized +- **403** – Access Denied +- **404** – Folder not found From 7d89f19f6fafe0a171b81d994867796007ace1a4 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 21 Feb 2018 13:09:54 +0100 Subject: [PATCH 28/58] docs: dashboard and folder permissions http api --- .../sources/http_api/dashboard_permissions.md | 22 +-- docs/sources/http_api/folder_permissions.md | 149 ++++++++++++++++++ 2 files changed, 160 insertions(+), 11 deletions(-) create mode 100644 docs/sources/http_api/folder_permissions.md diff --git a/docs/sources/http_api/dashboard_permissions.md b/docs/sources/http_api/dashboard_permissions.md index 4b87dbf83c2..26aa1550d7c 100644 --- a/docs/sources/http_api/dashboard_permissions.md +++ b/docs/sources/http_api/dashboard_permissions.md @@ -11,7 +11,7 @@ parent = "http_api" # Dashboard Permissions API -This API can be used to update/get the permissions for a dashboard or a folder. +This API can be used to update/get the permissions for a dashboard. Permissions with `dashboardId=-1` are the default permissions for users with the Viewer and Editor roles. Permissions can be set for a user, a team or a role (Viewer or Editor). Permissions cannot be set for Admins - they always have access to everything. @@ -21,16 +21,16 @@ The permission levels for the permission field: - 2 = Edit - 4 = Admin -## Get permissions for a dashboard or folder +## Get permissions for a dashboard -`GET /api/dashboards/id/:dashboardId/acl` +`GET /api/dashboards/id/:dashboardId/permissions` -Gets all existing permissions for the dashboard or folder with the given `dashboardId`. +Gets all existing permissions for the dashboard with the given `dashboardId`. **Example request**: ```http -GET /api/dashboards/id/1/acl HTTP/1.1 +GET /api/dashboards/id/1/permissions HTTP/1.1 Accept: application/json Content-Type: application/json Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk @@ -90,18 +90,18 @@ Status Codes: - **200** - Ok - **401** - Unauthorized - **403** - Access denied -- **404** - Dashboard/folder not found +- **404** - Dashboard not found -## Update permissions for a dashboard or folder +## Update permissions for a dashboard -`POST /api/dashboards/id/:dashboardId/acl` +`POST /api/dashboards/id/:dashboardId/permissions` -Updates permissions for a dashboard or folder. This operation will remove existing permissions if they're not included in the request. +Updates permissions for a dashboard. This operation will remove existing permissions if they're not included in the request. **Example request**: ```http -POST /api/dashboards/id/1/acl +POST /api/dashboards/id/1/permissions Accept: application/json Content-Type: application/json Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk @@ -138,7 +138,7 @@ HTTP/1.1 200 OK Content-Type: application/json; charset=UTF-8 Content-Length: 35 -{"message":"Dashboard acl updated"} +{"message":"Dashboard permissions updated"} ``` Status Codes: diff --git a/docs/sources/http_api/folder_permissions.md b/docs/sources/http_api/folder_permissions.md new file mode 100644 index 00000000000..284ab70866f --- /dev/null +++ b/docs/sources/http_api/folder_permissions.md @@ -0,0 +1,149 @@ ++++ +title = "Folder Permissions HTTP API " +description = "Grafana Folder Permissions HTTP API" +keywords = ["grafana", "http", "documentation", "api", "folder", "permission", "permissions", "acl"] +aliases = ["/http_api/dashboardpermissions/"] +type = "docs" +[menu.docs] +name = "Folder Permissions" +parent = "http_api" ++++ + +# Folder Permissions API + +This API can be used to update/get the permissions for a folder. + +Permissions with `folderId=-1` are the default permissions for users with the Viewer and Editor roles. Permissions can be set for a user, a team or a role (Viewer or Editor). Permissions cannot be set for Admins - they always have access to everything. + +The permission levels for the permission field: + +- 1 = View +- 2 = Edit +- 4 = Admin + +## Get permissions for a folder + +`GET /api/folders/:uid/permissions` + +Gets all existing permissions for the folder with the given `uid`. + +**Example request**: + +```http +GET /api/folders/nErXDvCkzz/permissions HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +**Example Response** + +```http +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Content-Length: 551 + +[ + { + "id": 1, + "folderId": -1, + "created": "2017-06-20T02:00:00+02:00", + "updated": "2017-06-20T02:00:00+02:00", + "userId": 0, + "userLogin": "", + "userEmail": "", + "teamId": 0, + "team": "", + "role": "Viewer", + "permission": 1, + "permissionName": "View", + "uid": "nErXDvCkzz", + "title": "", + "slug": "", + "isFolder": false, + "url": "" + }, + { + "id": 2, + "dashboardId": -1, + "created": "2017-06-20T02:00:00+02:00", + "updated": "2017-06-20T02:00:00+02:00", + "userId": 0, + "userLogin": "", + "userEmail": "", + "teamId": 0, + "team": "", + "role": "Editor", + "permission": 2, + "permissionName": "Edit", + "uid": "", + "title": "", + "slug": "", + "isFolder": false, + "url": "" + } +] +``` + +Status Codes: + +- **200** - Ok +- **401** - Unauthorized +- **403** - Access denied +- **404** - Folder not found + +## Update permissions for a folder + +`POST /api/folders/:uid/permissions` + +Updates permissions for a folder. This operation will remove existing permissions if they're not included in the request. + +**Example request**: + +```http +POST /api/folders/nErXDvCkzz/permissions +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + + "items": [ + { + "role": "Viewer", + "permission": 1 + }, + { + "role": "Editor", + "permission": 2 + }, + { + "teamId": 1, + "permission": 1 + }, + { + "userId": 11, + "permission": 4 + } + ] +} +``` + +JSON body schema: + +- **items** - The permission items to add/update. Items that are omitted from the list will be removed. + +**Example response**: + +```http +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Content-Length: 35 + +{"message":"Folder permissions updated"} +``` + +Status Codes: + +- **200** - Ok +- **401** - Unauthorized +- **403** - Access denied +- **404** - Dashboard not found From 02b412b127ec6ca0890fddae4dad2af45bfe83d3 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 21 Feb 2018 13:11:32 +0100 Subject: [PATCH 29/58] docs: update http api index --- docs/sources/http_api/index.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/sources/http_api/index.md b/docs/sources/http_api/index.md index b00deaf6715..81b3c907c7a 100644 --- a/docs/sources/http_api/index.md +++ b/docs/sources/http_api/index.md @@ -21,6 +21,9 @@ dashboards, creating users and updating data sources. * [Authentication API]({{< relref "/http_api/auth.md" >}}) * [Dashboard API]({{< relref "/http_api/dashboard.md" >}}) * [Dashboard Versions API]({{< relref "http_api/dashboard_versions.md" >}}) +* [Dashboard Permissions API]({{< relref "http_api/dashboard_permissions.md" >}}) +* [Folder API]({{< relref "/http_api/folder.md" >}}) +* [Folder Permissions API]({{< relref "http_api/folder_permissions.md" >}}) * [Folder/dashboard search API]({{< relref "/http_api/folder_dashboard_search.md" >}}) * [Data Source API]({{< relref "http_api/data_source.md" >}}) * [Organisation API]({{< relref "http_api/org.md" >}}) From c367cb294e3a6ecdd5b9bd9d28c3f06f537db683 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 22 Feb 2018 16:07:55 +0100 Subject: [PATCH 30/58] docs: minor folder http api changes --- docs/sources/http_api/folder.md | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/docs/sources/http_api/folder.md b/docs/sources/http_api/folder.md index 13de0aab6c2..7ee1f737799 100644 --- a/docs/sources/http_api/folder.md +++ b/docs/sources/http_api/folder.md @@ -159,26 +159,6 @@ Status Codes: - **400** – Errors (invalid json, missing or invalid fields, etc) - **401** – Unauthorized - **403** – Access Denied -- **412** – Precondition failed - -The **412** status code is used for explaing that you cannot create the folder and why. -There can be different reasons for this: - -- A folder or dashboard in the general folder with the same name already exists, `status=name-exists` -- A folder/dashboard with the same uid already exists, `status=uid-exists` - - The response body will have the following properties: - -```http -HTTP/1.1 412 Precondition Failed -Content-Type: application/json; charset=UTF-8 -Content-Length: 97 - -{ - "message": "A folder or dashboard in the general folder with the same name already exists", - "status": "name-exists" -} -``` ## Update folder @@ -202,6 +182,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk JSON Body schema: +- **uid** – Provide another [unique identifier](/http_api/folder/#identifier-id-vs-unique-identifier-uid) than stored to change the unique identifier. - **title** – The title of the folder. - **version** – Provide the current version to be able to update the folder. Not needed if `overwrite=true`. - **overwrite** – Set to true if you want to overwrite existing folder with newer version. @@ -241,7 +222,6 @@ Status Codes: The **412** status code is used for explaing that you cannot update the folder and why. There can be different reasons for this: -- A folder or dashboard in the general folder with the same name already exists, `status=name-exists` - The folder has been changed by someone else, `status=version-mismatch` The response body will have the following properties: From 0ac6d7cf4cad895642f678d5d7b81faf64b2200e Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 22 Feb 2018 18:24:10 +0100 Subject: [PATCH 31/58] docs: update shortcut docs --- docs/sources/features/shortcuts.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/sources/features/shortcuts.md b/docs/sources/features/shortcuts.md index 66baddcc91c..cbcf3670c83 100644 --- a/docs/sources/features/shortcuts.md +++ b/docs/sources/features/shortcuts.md @@ -34,6 +34,8 @@ Hit `?` on your keyboard to open the shortcuts help modal. - `d` `s` Dashboard settings - `d` `v` Toggle in-active / view mode - `d` `k` Toggle kiosk mode (hides top nav) +- `d` `E` Expand all rows +- `d` `C` Collapse all rows - `mod+o` Toggle shared graph crosshair ### Focused Panel @@ -42,12 +44,9 @@ Hit `?` on your keyboard to open the shortcuts help modal. - `p` `s` Open Panel Share Modal - `p` `r` Remove Panel -### Focused Row -- `r` `c` Collapse Row -- `r` `r` Remove Row - ### Time Range - `t` `z` Zoom out time range - `t` Move time range back - `t` Move time range forward +mod = CTRL on windows or linux and CMD key on Mac From b316dfea988f1362f24765074302ed76287e7cc1 Mon Sep 17 00:00:00 2001 From: Brian Gann Date: Thu, 15 Feb 2018 11:44:03 -0600 Subject: [PATCH 32/58] feature for issue #9911 --- .../templating/specs/template_srv.jest.ts | 80 ++++++++++++++++++- .../app/features/templating/template_srv.ts | 23 +++--- 2 files changed, 91 insertions(+), 12 deletions(-) diff --git a/public/app/features/templating/specs/template_srv.jest.ts b/public/app/features/templating/specs/template_srv.jest.ts index 577669a41dd..37e2f5e4fe5 100644 --- a/public/app/features/templating/specs/template_srv.jest.ts +++ b/public/app/features/templating/specs/template_srv.jest.ts @@ -31,12 +31,40 @@ describe('templateSrv', function() { expect(target).toBe('this.mupp.filters'); }); + it('should replace ${test} with scoped value', function() { + var target = _templateSrv.replace('this.${test}.filters', { + test: { value: 'mupp', text: 'asd' }, + }); + expect(target).toBe('this.mupp.filters'); + }); + + it('should replace ${test:glob} with scoped value', function() { + var target = _templateSrv.replace('this.${test:glob}.filters', { + test: { value: 'mupp', text: 'asd' }, + }); + expect(target).toBe('this.mupp.filters'); + }); + it('should replace $test with scoped text', function() { var target = _templateSrv.replaceWithText('this.$test.filters', { test: { value: 'mupp', text: 'asd' }, }); expect(target).toBe('this.asd.filters'); }); + + it('should replace ${test} with scoped text', function() { + var target = _templateSrv.replaceWithText('this.${test}.filters', { + test: { value: 'mupp', text: 'asd' }, + }); + expect(target).toBe('this.asd.filters'); + }); + + it('should replace ${test:glob} with scoped text', function() { + var target = _templateSrv.replaceWithText('this.${test:glob}.filters', { + test: { value: 'mupp', text: 'asd' }, + }); + expect(target).toBe('this.asd.filters'); + }); }); describe('getAdhocFilters', function() { @@ -79,18 +107,34 @@ describe('templateSrv', function() { ]); }); + it('should replace $test with globbed value', function() { var target = _templateSrv.replace('this.$test.filters', {}, 'glob'); expect(target).toBe('this.{value1,value2}.filters'); }); + it('should replace ${test} with globbed value', function() { + var target = _templateSrv.replace('this.${test}.filters', {}, 'glob'); + expect(target).toBe('this.{value1,value2}.filters'); + }); + + it('should replace ${test:glob} with globbed value', function() { + var target = _templateSrv.replace('this.${test:glob}.filters', {}); + expect(target).toBe('this.{value1,value2}.filters'); + }); + it('should replace $test with piped value', function() { var target = _templateSrv.replace('this=$test', {}, 'pipe'); expect(target).toBe('this=value1|value2'); }); - it('should replace $test with piped value', function() { - var target = _templateSrv.replace('this=$test', {}, 'pipe'); + it('should replace ${test} with piped value', function() { + var target = _templateSrv.replace('this=${test}', {}, 'pipe'); + expect(target).toBe('this=value1|value2'); + }); + + it('should replace ${test:pipe} with piped value', function() { + var target = _templateSrv.replace('this=${test:pipe}', {}); expect(target).toBe('this=value1|value2'); }); }); @@ -111,6 +155,16 @@ describe('templateSrv', function() { var target = _templateSrv.replace('this.$test.filters', {}, 'glob'); expect(target).toBe('this.{value1,value2}.filters'); }); + + it('should replace ${test} with formatted all value', function() { + var target = _templateSrv.replace('this.${test}.filters', {}, 'glob'); + expect(target).toBe('this.{value1,value2}.filters'); + }); + + it('should replace ${test:glob} with formatted all value', function() { + var target = _templateSrv.replace('this.${test:glob}.filters', {}); + expect(target).toBe('this.{value1,value2}.filters'); + }); }); describe('variable with all option and custom value', function() { @@ -131,6 +185,16 @@ describe('templateSrv', function() { expect(target).toBe('this.*.filters'); }); + it('should replace ${test} with formatted all value', function() { + var target = _templateSrv.replace('this.${test}.filters', {}, 'glob'); + expect(target).toBe('this.*.filters'); + }); + + it('should replace ${test:glob} with formatted all value', function() { + var target = _templateSrv.replace('this.${test:glob}.filters', {}); + expect(target).toBe('this.*.filters'); + }); + it('should not escape custom all value', function() { var target = _templateSrv.replace('this.$test', {}, 'regex'); expect(target).toBe('this.*'); @@ -143,6 +207,18 @@ describe('templateSrv', function() { var target = _templateSrv.replace('this:$test', {}, 'lucene'); expect(target).toBe('this:value\\/4'); }); + + it('should properly escape ${test} with lucene escape sequences', function() { + initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]); + var target = _templateSrv.replace('this:${test}', {}, 'lucene'); + expect(target).toBe('this:value\\/4'); + }); + + it('should properly escape ${test:lucene} with lucene escape sequences', function() { + initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]); + var target = _templateSrv.replace('this:${test:lucene}', {}); + expect(target).toBe('this:value\\/4'); + }); }); describe('format variable to string values', function() { diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index fa5cabd847a..f29e133238e 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -8,7 +8,7 @@ function luceneEscape(value) { export class TemplateSrv { variables: any[]; - private regex = /\$(\w+)|\[\[([\s\S]+?)\]\]/g; + private regex = /\$(\w+)|\[\[([\s\S]+?)\]\]|\${(\w+):?(\w+)?}/g; private index = {}; private grafanaVariables = {}; private builtIns = {}; @@ -89,6 +89,9 @@ export class TemplateSrv { } var escapedValues = _.map(value, kbn.regexEscape); + if (escapedValues.length === 1) { + return escapedValues[0]; + } return '(' + escapedValues.join('|') + ')'; } case 'lucene': { @@ -140,8 +143,8 @@ export class TemplateSrv { str = _.escape(str); this.regex.lastIndex = 0; - return str.replace(this.regex, (match, g1, g2) => { - if (this.index[g1 || g2] || this.builtIns[g1 || g2]) { + return str.replace(this.regex, (match, g1, g2, g3) => { + if (this.index[g1 || g2 || g3] || this.builtIns[g1 || g2 || g3]) { return '' + match + ''; } return match; @@ -167,11 +170,11 @@ export class TemplateSrv { var variable, systemValue, value; this.regex.lastIndex = 0; - return target.replace(this.regex, (match, g1, g2) => { - variable = this.index[g1 || g2]; - + return target.replace(this.regex, (match, g1, g2, g3, g4) => { + variable = this.index[g1 || g2 || g3]; + format = g4 || format; if (scopedVars) { - value = scopedVars[g1 || g2]; + value = scopedVars[g1 || g2 || g3]; if (value) { return this.formatValue(value.value, format, variable); } @@ -212,15 +215,15 @@ export class TemplateSrv { var variable; this.regex.lastIndex = 0; - return target.replace(this.regex, (match, g1, g2) => { + return target.replace(this.regex, (match, g1, g2, g3) => { if (scopedVars) { - var option = scopedVars[g1 || g2]; + var option = scopedVars[g1 || g2 || g3]; if (option) { return option.text; } } - variable = this.index[g1 || g2]; + variable = this.index[g1 || g2 || g3]; if (!variable) { return match; } From fa193fa88837cb9b496bf21443c1e05ba32fa8cf Mon Sep 17 00:00:00 2001 From: Brian Gann Date: Thu, 15 Feb 2018 11:49:26 -0600 Subject: [PATCH 33/58] offer template variables for tags --- public/app/plugins/datasource/graphite/query_ctrl.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/app/plugins/datasource/graphite/query_ctrl.ts b/public/app/plugins/datasource/graphite/query_ctrl.ts index 29ff29b7ae3..0563de61705 100644 --- a/public/app/plugins/datasource/graphite/query_ctrl.ts +++ b/public/app/plugins/datasource/graphite/query_ctrl.ts @@ -348,6 +348,10 @@ export class GraphiteQueryCtrl extends QueryCtrl { let tagKey = tag.key; return this.datasource.getTagValuesAutoComplete(tagExpressions, tagKey, valuePrefix).then(values => { let altValues = _.map(values, 'text'); + // Add template variables as additional values + _.eachRight(this.templateSrv.variables, variable => { + altValues.push('${' + variable.name + ':regex}'); + }); return mapToDropdownOptions(altValues); }); } From dc6f977304c509d58b47f7e990ad2a3ea79666f7 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Thu, 22 Feb 2018 18:39:54 -0500 Subject: [PATCH 34/58] support [[variable:type]] syntax --- .../app/features/templating/template_srv.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index f29e133238e..a5b5418a564 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -8,7 +8,7 @@ function luceneEscape(value) { export class TemplateSrv { variables: any[]; - private regex = /\$(\w+)|\[\[([\s\S]+?)\]\]|\${(\w+):?(\w+)?}/g; + private regex = /\$(\w+)|\[\[([\s\S]+?)(?::(\w+))?\]\]|\${(\w+)(?::(\w+))?}/g; private index = {}; private grafanaVariables = {}; private builtIns = {}; @@ -143,8 +143,8 @@ export class TemplateSrv { str = _.escape(str); this.regex.lastIndex = 0; - return str.replace(this.regex, (match, g1, g2, g3) => { - if (this.index[g1 || g2 || g3] || this.builtIns[g1 || g2 || g3]) { + return str.replace(this.regex, (match, g1, g2, g3, g4) => { + if (this.index[g1 || g2 || g4] || this.builtIns[g1 || g2 || g4]) { return '' + match + ''; } return match; @@ -170,11 +170,11 @@ export class TemplateSrv { var variable, systemValue, value; this.regex.lastIndex = 0; - return target.replace(this.regex, (match, g1, g2, g3, g4) => { - variable = this.index[g1 || g2 || g3]; - format = g4 || format; + return target.replace(this.regex, (match, g1, g2, g3, g4, g5) => { + variable = this.index[g1 || g2 || g4]; + format = g3 || g5 || format; if (scopedVars) { - value = scopedVars[g1 || g2 || g3]; + value = scopedVars[g1 || g2 || g4]; if (value) { return this.formatValue(value.value, format, variable); } @@ -215,15 +215,15 @@ export class TemplateSrv { var variable; this.regex.lastIndex = 0; - return target.replace(this.regex, (match, g1, g2, g3) => { + return target.replace(this.regex, (match, g1, g2, g3, g4) => { if (scopedVars) { - var option = scopedVars[g1 || g2 || g3]; + var option = scopedVars[g1 || g2 || g4]; if (option) { return option.text; } } - variable = this.index[g1 || g2 || g3]; + variable = this.index[g1 || g2 || g4]; if (!variable) { return match; } From 01fb3ea0346384ed01fde7841194d5d464fb9ed8 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 23 Feb 2018 09:32:22 +0100 Subject: [PATCH 35/58] docs: adds accesskey and secret to securejsonfields closes #11005 --- docs/sources/administration/provisioning.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index 3cb7f7d552b..140053ec541 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -169,6 +169,8 @@ Secure json data is a map of settings that will be encrypted with [secret key](/ | tlsClientKey | string | *All* |TLS Client key for outgoing requests | | password | string | Postgre | password | | user | string | Postgre | user | +| accessKey | string | Cloudwatch | Access key for connecting to Cloudwatch | +| secretKey | string | Cloudwatch | Secret key for connecting to Cloudwatch | ### Dashboards From abf498fc7f4523c44f6aacfb1070e55829059f6d Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Fri, 23 Feb 2018 09:51:19 -0500 Subject: [PATCH 36/58] improve maintainability --- .../app/features/templating/template_srv.ts | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index a5b5418a564..40f119ea10b 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -8,6 +8,12 @@ function luceneEscape(value) { export class TemplateSrv { variables: any[]; + /* + * This regex matches 3 types of variable reference with an optional format specifier + * \$(\w+) $var1 + * \[\[([\s\S]+?)(?::(\w+))?\]\] [[var2]] or [[var2:fmt2]] + * \${(\w+)(?::(\w+))?} ${var3} or ${var3:fmt3} + */ private regex = /\$(\w+)|\[\[([\s\S]+?)(?::(\w+))?\]\]|\${(\w+)(?::(\w+))?}/g; private index = {}; private grafanaVariables = {}; @@ -143,8 +149,8 @@ export class TemplateSrv { str = _.escape(str); this.regex.lastIndex = 0; - return str.replace(this.regex, (match, g1, g2, g3, g4) => { - if (this.index[g1 || g2 || g4] || this.builtIns[g1 || g2 || g4]) { + return str.replace(this.regex, (match, var1, var2, fmt2, var3) => { + if (this.index[var1 || var2 || var3] || this.builtIns[var1 || var2 || var3]) { return '' + match + ''; } return match; @@ -170,11 +176,11 @@ export class TemplateSrv { var variable, systemValue, value; this.regex.lastIndex = 0; - return target.replace(this.regex, (match, g1, g2, g3, g4, g5) => { - variable = this.index[g1 || g2 || g4]; - format = g3 || g5 || format; + return target.replace(this.regex, (match, var1, var2, fmt2, var3, fmt3) => { + variable = this.index[var1 || var2 || var3]; + format = fmt2 || fmt3 || format; if (scopedVars) { - value = scopedVars[g1 || g2 || g4]; + value = scopedVars[var1 || var2 || var3]; if (value) { return this.formatValue(value.value, format, variable); } @@ -215,15 +221,15 @@ export class TemplateSrv { var variable; this.regex.lastIndex = 0; - return target.replace(this.regex, (match, g1, g2, g3, g4) => { + return target.replace(this.regex, (match, var1, var2, fmt2, var3) => { if (scopedVars) { - var option = scopedVars[g1 || g2 || g4]; + var option = scopedVars[var1 || var2 || var3]; if (option) { return option.text; } } - variable = this.index[g1 || g2 || g4]; + variable = this.index[var1 || var2 || var3]; if (!variable) { return match; } From 1a170133ca7be76f3a7860c037d26d30e5c0629f Mon Sep 17 00:00:00 2001 From: Marco Tulio R Braga Date: Mon, 26 Feb 2018 03:04:39 -0300 Subject: [PATCH 37/58] [doc] Fix extra alerting options in installation->configuration --- docs/sources/installation/configuration.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 5dc2ee651b2..be9ac44e827 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -875,6 +875,4 @@ Defaults to true. Set to false to disable alerting engine and hide Alerting from ### execute_alerts -### execute_alerts = true - Makes it possible to turn off alert rule execution. From c21d9deccb62873dbae9fb3483e7880aa9f03ed7 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 26 Feb 2018 09:44:03 +0100 Subject: [PATCH 38/58] build: update to version 5.0.0-beta5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3dde98f0f39..5f20f8ffc73 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "company": "Grafana Labs" }, "name": "grafana", - "version": "5.0.0-beta4", + "version": "5.0.0-beta5", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git" From ef67d20971310439efbd8dd28cb37fd9843fc5a1 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 26 Feb 2018 10:49:43 +0100 Subject: [PATCH 39/58] changelog: update for v5.0.0-beta5 --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d16e816f3d..c76346e419e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# 5.0.0-beta5 (2018-02-26) + +### Fixes + +- **Orgs** Unable to switch org when too many orgs listed [#10774](https://github.com/grafana/grafana/issues/10774) +- **Folders** Make it easier/explicit to access/modify folders using the API [#10630](https://github.com/grafana/grafana/issues/10630) +- **Dashboard** Scrollbar works incorrectly in Grafana 5.0 Beta4 in some cases [#10982](https://github.com/grafana/grafana/issues/10982) +- **ElasticSearch** Custom aggregation sizes no longer allowed for Elasticsearch [#10124](https://github.com/grafana/grafana/issues/10124) +- **oauth** Github OAuth with allowed organizations fails to login [#10964](https://github.com/grafana/grafana/issues/10964) +- **heatmap** Heatmap panel has partially hidden legend [#10793](https://github.com/grafana/grafana/issues/10793) +- **snapshots** Expired snapshots not being cleaned up [#10996](https://github.com/grafana/grafana/pull/10996) + # 5.0.0-beta4 (2018-02-19) ### Fixes From 6f4c043dd672b7f4ac6f25d00b9b22ebbeb597cf Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 26 Feb 2018 11:04:08 +0100 Subject: [PATCH 40/58] docs: update to install pages for beta5 --- docs/sources/installation/debian.md | 6 +++--- docs/sources/installation/rpm.md | 4 ++-- docs/sources/installation/windows.md | 2 +- packaging/publish/publish_testing.sh | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index a45afe35e6b..bb85a579a0c 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -16,7 +16,7 @@ weight = 1 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) -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) +Beta for Debian-based Linux | [grafana_5.0.0-beta5_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.0-beta5_amd64.deb) Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing installation. @@ -33,9 +33,9 @@ sudo dpkg -i grafana_4.6.3_amd64.deb ## Install Latest Beta ```bash -wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.0-beta4_amd64.deb +wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.0-beta5_amd64.deb sudo apt-get install -y adduser libfontconfig -sudo dpkg -i grafana_5.0.0-beta4_amd64.deb +sudo dpkg -i grafana_5.0.0-beta5_amd64.deb ``` ## APT Repository diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index e97ab17a697..1e0645e74c1 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -16,7 +16,7 @@ weight = 2 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) -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) +Latest Beta for CentOS / Fedora / OpenSuse / Redhat Linux | [5.0.0-beta5 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta5.x86_64.rpm) Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing installation. @@ -32,7 +32,7 @@ $ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/g ## Install Beta ```bash -$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta4.x86_64.rpm +$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta5.x86_64.rpm ``` Or install manually using `rpm`. diff --git a/docs/sources/installation/windows.md b/docs/sources/installation/windows.md index 07f5abde52f..90c469bfe59 100644 --- a/docs/sources/installation/windows.md +++ b/docs/sources/installation/windows.md @@ -14,7 +14,7 @@ weight = 3 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 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) +Latest beta package for Windows | [grafana.5.0.0-beta5.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta5.windows-x64.zip) Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing installation. 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 From 7fb34f7918173ebed4e9d8a2992a2beb2736628a Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Mon, 26 Feb 2018 11:52:48 +0100 Subject: [PATCH 41/58] gave scroll-canvas-dashboard 100% height in kiosk-mode, fixes #11010 (#11017) --- public/sass/components/_view_states.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/sass/components/_view_states.scss b/public/sass/components/_view_states.scss index be5361ceeb4..b1fa47d0c0a 100644 --- a/public/sass/components/_view_states.scss +++ b/public/sass/components/_view_states.scss @@ -3,6 +3,9 @@ .navbar { display: none; } + .scroll-canvas--dashboard { + height: 100%; + } } .playlist-active, From dff2c14aedca25c360b30bb08f0a318050a00d89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 27 Feb 2018 11:34:29 +0100 Subject: [PATCH 42/58] fix: changed react-grid-layout to use grafana fork to a commit before https://github.com/STRML/react-grid-layout/commit/15503084fb7b0af826427c8c0706901e5745a39f, this fixes all the panel movement bugs, fixes #10831 --- package.json | 2 +- .../features/dashboard/dashgrid/DashboardGrid.tsx | 10 ++++++---- public/sass/components/_dashboard_grid.scss | 2 +- yarn.lock | 15 +++++++++++---- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 5f20f8ffc73..b715d96dcde 100644 --- a/package.json +++ b/package.json @@ -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/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 3f65c33c90d..03bf65afc6e 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import ReactGridLayout from 'react-grid-layout'; +import ReactGridLayout from 'react-grid-layout-grafana'; import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants'; import { DashboardPanel } from './DashboardPanel'; import { DashboardModel } from '../dashboard_model'; @@ -50,7 +50,8 @@ function GridWrapper({ onResize={onResize} onResizeStop={onResizeStop} onDragStop={onDragStop} - onLayoutChange={onLayoutChange}> + onLayoutChange={onLayoutChange} + > {children} ); @@ -178,7 +179,7 @@ export class DashboardGrid extends React.Component { panelElements.push(
-
, + ); } @@ -196,7 +197,8 @@ export class DashboardGrid extends React.Component { onWidthChange={this.onWidthChange} onDragStop={this.onDragStop} onResize={this.onResize} - onResizeStop={this.onResizeStop}> + onResizeStop={this.onResizeStop} + > {this.renderPanels()} ); diff --git a/public/sass/components/_dashboard_grid.scss b/public/sass/components/_dashboard_grid.scss index 0a27df75164..dd81a210409 100644 --- a/public/sass/components/_dashboard_grid.scss +++ b/public/sass/components/_dashboard_grid.scss @@ -1,4 +1,4 @@ -@import '~react-grid-layout/css/styles.css'; +@import '~react-grid-layout-grafana/css/styles.css'; @import '~react-resizable/css/styles.css'; .panel-in-fullscreen { diff --git a/yarn.lock b/yarn.lock index 692eb83ea96..2de60f03c27 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8275,16 +8275,23 @@ react-dom@^16.2.0: object-assign "^4.1.1" prop-types "^15.6.0" -"react-draggable@^2.2.6 || ^3.0.3", react-draggable@^3.0.3: +"react-draggable@^2.2.6 || ^3.0.3": version "3.0.3" resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-3.0.3.tgz#a6f9b3a7171981b76dadecf238316925cb9eacf4" dependencies: classnames "^2.2.5" prop-types "^15.5.10" -react-grid-layout@^0.16.2: - version "0.16.2" - resolved "https://registry.yarnpkg.com/react-grid-layout/-/react-grid-layout-0.16.2.tgz#ef09b0b6db4a9635799663658277ee2d26fa2994" +react-draggable@^3.0.3: + version "3.0.5" + resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-3.0.5.tgz#c031e0ed4313531f9409d6cd84c8ebcec0ddfe2d" + dependencies: + classnames "^2.2.5" + prop-types "^15.6.0" + +react-grid-layout-grafana@0.16.0: + version "0.16.0" + resolved "https://registry.yarnpkg.com/react-grid-layout-grafana/-/react-grid-layout-grafana-0.16.0.tgz#12242153fcd0bb80a26af8e41694bc2fde788b3a" dependencies: classnames "2.x" lodash.isequal "^4.0.0" From 7d91b737749fa0293a53fbc115ad798ad26c1ab3 Mon Sep 17 00:00:00 2001 From: jganseman Date: Tue, 27 Feb 2018 13:26:53 +0100 Subject: [PATCH 43/58] grammar fix, add dir, and remove redundant info --- docs/sources/installation/windows.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/sources/installation/windows.md b/docs/sources/installation/windows.md index 90c469bfe59..5e8a204d11d 100644 --- a/docs/sources/installation/windows.md +++ b/docs/sources/installation/windows.md @@ -31,9 +31,9 @@ on windows. Edit `custom.ini` and uncomment the `http_port` configuration option (`;` is the comment character in ini files) and change it to something like `8080` or similar. That port should not require extra Windows privileges. -Start Grafana by executing `grafana-server.exe`, preferably from the +Start Grafana by executing `grafana-server.exe`, located in the `bin` directory, preferably from the command line. If you want to run Grafana as windows service, download -[NSSM](https://nssm.cc/). It is very easy add Grafana as a Windows +[NSSM](https://nssm.cc/). It is very easy to add Grafana as a Windows service using that tool. Read more about the [configuration options]({{< relref "configuration.md" >}}). @@ -43,7 +43,3 @@ Read more about the [configuration options]({{< relref "configuration.md" >}}). The Grafana backend includes Sqlite3 which requires GCC to compile. So in order to compile Grafana on Windows you need to install GCC. We recommend [TDM-GCC](http://tdm-gcc.tdragon.net/download). - -Copy `conf/sample.ini` to a file named `conf/custom.ini` and change the -web server port to something like 8080. The default Grafana port, 3000, -requires special privileges on Windows. From 69006a7764e93c4ae2192fa5346b0a7db7ce2453 Mon Sep 17 00:00:00 2001 From: Marcel Anacker Date: Tue, 27 Feb 2018 15:48:36 +0100 Subject: [PATCH 44/58] Alerting: Fix OK state doesn't show up in Microsoft Teams (#11032) * Alerting: Fix OK state doesn't show up in Microsoft Teams * Fix formatting --- pkg/services/alerting/notifiers/teams.go | 2 ++ 1 file changed, 2 insertions(+) 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{}{ From 955dfcc8fe364bc7e3ff8ebbaaaab3ac12fc8cfc Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 26 Feb 2018 19:12:01 +0100 Subject: [PATCH 45/58] dashboards: don't allow override of permissions with a lower precedence If a dashboard inherits permissions from a folder, don't allow same permission to be added to the dashboard with a lower permission. Add backend validation so that you cannot add same permission to folder/dashboard, for example same user/team with different permissions --- pkg/services/guardian/guardian.go | 50 +- pkg/services/guardian/guardian_test.go | 659 +++++++++++++++++++++++++ 2 files changed, 702 insertions(+), 7 deletions(-) create mode 100644 pkg/services/guardian/guardian_test.go diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go index 23d43a53f35..972e87de39b 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 ( + ErrGuardianDuplicatePermission = errors.New("You cannot add multiple permissions for a user, team or role") + ErrGuardianOverrideLowerPresedence = errors.New("You cannot override a permission with a lower presedence permission") +) + // DashboardGuardian to be used for guard against operations without access on dashboard and acl type DashboardGuardian interface { CanSave() (bool, error) @@ -119,14 +126,41 @@ 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{} for _, p := range updatePermissions { - acl = append(acl, &m.DashboardAclInfoDTO{UserId: p.UserId, TeamId: p.TeamId, Role: p.Role, Permission: p.Permission}) + for _, a := range acl { + if (a.UserId <= 0 && a.TeamId <= 0 && a.UserId == p.UserId && a.TeamId == p.TeamId && a.Role == p.Role) || + (a.UserId > 0 && a.UserId == p.UserId) || + (a.TeamId > 0 && a.TeamId == p.TeamId) { + return false, ErrGuardianDuplicatePermission + } + } + + acl = append(acl, &m.DashboardAclInfoDTO{DashboardId: p.DashboardId, UserId: p.UserId, TeamId: p.TeamId, Role: p.Role, Permission: p.Permission}) + } + + existingPermissions, err := g.GetAcl() + if err != nil { + return false, err + } + + for _, a := range acl { + for _, existingPerm := range existingPermissions { + if a.DashboardId == existingPerm.DashboardId { + continue + } + + if (a.UserId <= 0 && a.TeamId <= 0 && a.UserId == existingPerm.UserId && a.TeamId == existingPerm.TeamId && *a.Role == *existingPerm.Role && a.Permission <= existingPerm.Permission) || + (a.UserId > 0 && a.UserId == existingPerm.UserId && a.Permission <= existingPerm.Permission) || + (a.TeamId > 0 && a.TeamId == existingPerm.TeamId && a.Permission <= existingPerm.Permission) { + return false, ErrGuardianOverrideLowerPresedence + } + } + } + + if g.user.OrgRole == m.ROLE_ADMIN { + return true, nil } return g.checkAcl(permission, acl) @@ -169,6 +203,8 @@ type FakeDashboardGuardian struct { CanAdminValue bool HasPermissionValue bool CheckPermissionBeforeUpdateValue bool + CheckPermissionBeforeUpdateError error + GetAclValue []*m.DashboardAclInfoDTO } func (g *FakeDashboardGuardian) CanSave() (bool, error) { @@ -192,11 +228,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..d420d1add97 --- /dev/null +++ b/pkg/services/guardian/guardian_test.go @@ -0,0 +1,659 @@ +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/role 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, ErrGuardianDuplicatePermission) + }) + + Convey("With duplicate team/role 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, ErrGuardianDuplicatePermission) + }) + + Convey("With duplicate everyone/role permissions should return error", func() { + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 1, Permission: m.PERMISSION_VIEW}, + {OrgId: 1, DashboardId: 1, Permission: m.PERMISSION_ADMIN}, + } + _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(err, ShouldEqual, ErrGuardianDuplicatePermission) + }) + }) + + 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, ErrGuardianOverrideLowerPresedence) + }) + + 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, ErrGuardianOverrideLowerPresedence) + }) + + 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, ErrGuardianOverrideLowerPresedence) + }) + }) + + 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, ErrGuardianOverrideLowerPresedence) + }) + + 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, ErrGuardianOverrideLowerPresedence) + }) + }) + + 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, ErrGuardianOverrideLowerPresedence) + }) + }) + + 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, ErrGuardianOverrideLowerPresedence) + }) + + 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, ErrGuardianOverrideLowerPresedence) + }) + + 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, ErrGuardianOverrideLowerPresedence) + }) + }) + + 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, ErrGuardianOverrideLowerPresedence) + }) + + 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, ErrGuardianOverrideLowerPresedence) + }) + }) + + 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, ErrGuardianOverrideLowerPresedence) + }) + }) + + 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, ErrGuardianOverrideLowerPresedence) + }) + + 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, ErrGuardianOverrideLowerPresedence) + }) + }) + + 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, ErrGuardianOverrideLowerPresedence) + }) + }) + }) + + 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) +} From 3c14cecd506feb99711efaac0580e0ab6c134c26 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 26 Feb 2018 20:14:21 +0100 Subject: [PATCH 46/58] folders: handle new guardian error responses and add tests --- pkg/api/folder_permission.go | 17 +++-- pkg/api/folder_permission_test.go | 119 +++++++++++++++++++++++++++++- 2 files changed, 129 insertions(+), 7 deletions(-) diff --git a/pkg/api/folder_permission.go b/pkg/api/folder_permission.go index 7453552d092..c4c762dc3cd 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.ErrGuardianDuplicatePermission || + err == guardian.ErrGuardianOverrideLowerPresedence { + 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..bc35031ce52 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.ErrGuardianDuplicatePermission, + }) + + 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.ErrGuardianOverrideLowerPresedence}, + ) + + 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 + }) + }) }) } From 03f8eff8800176759fcb971cfadcb55b6746534b Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 26 Feb 2018 20:15:57 +0100 Subject: [PATCH 47/58] dashboards: handle new guardian error responses and update tests Using a mocked guardian instead and relies on the actual guardian tests for verifying permission handling correctness --- pkg/api/dashboard_permission.go | 17 +- pkg/api/dashboard_permission_test.go | 290 +++++++++++++-------------- 2 files changed, 154 insertions(+), 153 deletions(-) diff --git a/pkg/api/dashboard_permission.go b/pkg/api/dashboard_permission.go index 351b81885d6..dafa5777803 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.ErrGuardianDuplicatePermission || + err == guardian.ErrGuardianOverrideLowerPresedence { + 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..0bb9908df6d 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.ErrGuardianDuplicatePermission, + }) + + 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.ErrGuardianOverrideLowerPresedence}, + ) + + 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) }) From f76b98d252b70b10a716c9e6329d8a9917006280 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 27 Feb 2018 16:03:11 +0100 Subject: [PATCH 48/58] dashboards: change dashboard/folder permission error messages --- pkg/api/dashboard_permission.go | 4 +-- pkg/api/dashboard_permission_test.go | 4 +-- pkg/api/folder_permission.go | 4 +-- pkg/api/folder_permission_test.go | 4 +-- pkg/services/guardian/guardian.go | 8 +++--- pkg/services/guardian/guardian_test.go | 36 +++++++++++++------------- 6 files changed, 30 insertions(+), 30 deletions(-) diff --git a/pkg/api/dashboard_permission.go b/pkg/api/dashboard_permission.go index dafa5777803..419825644c8 100644 --- a/pkg/api/dashboard_permission.go +++ b/pkg/api/dashboard_permission.go @@ -69,8 +69,8 @@ func UpdateDashboardPermissions(c *middleware.Context, apiCmd dtos.UpdateDashboa if okToUpdate, err := g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, cmd.Items); err != nil || !okToUpdate { if err != nil { - if err == guardian.ErrGuardianDuplicatePermission || - err == guardian.ErrGuardianOverrideLowerPresedence { + if err == guardian.ErrGuardianPermissionExists || + err == guardian.ErrGuardianOverride { return ApiError(400, err.Error(), err) } diff --git a/pkg/api/dashboard_permission_test.go b/pkg/api/dashboard_permission_test.go index 0bb9908df6d..03231338268 100644 --- a/pkg/api/dashboard_permission_test.go +++ b/pkg/api/dashboard_permission_test.go @@ -119,7 +119,7 @@ func TestDashboardPermissionApiEndpoint(t *testing.T) { guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ CanAdminValue: true, CheckPermissionBeforeUpdateValue: false, - CheckPermissionBeforeUpdateError: guardian.ErrGuardianDuplicatePermission, + CheckPermissionBeforeUpdateError: guardian.ErrGuardianPermissionExists, }) getDashboardQueryResult := m.NewDashboard("Dash") @@ -149,7 +149,7 @@ func TestDashboardPermissionApiEndpoint(t *testing.T) { guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ CanAdminValue: true, CheckPermissionBeforeUpdateValue: false, - CheckPermissionBeforeUpdateError: guardian.ErrGuardianOverrideLowerPresedence}, + CheckPermissionBeforeUpdateError: guardian.ErrGuardianOverride}, ) getDashboardQueryResult := m.NewDashboard("Dash") diff --git a/pkg/api/folder_permission.go b/pkg/api/folder_permission.go index c4c762dc3cd..7c8aba87337 100644 --- a/pkg/api/folder_permission.go +++ b/pkg/api/folder_permission.go @@ -78,8 +78,8 @@ func UpdateFolderPermissions(c *middleware.Context, apiCmd dtos.UpdateDashboardA if okToUpdate, err := g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, cmd.Items); err != nil || !okToUpdate { if err != nil { - if err == guardian.ErrGuardianDuplicatePermission || - err == guardian.ErrGuardianOverrideLowerPresedence { + if err == guardian.ErrGuardianPermissionExists || + err == guardian.ErrGuardianOverride { return ApiError(400, err.Error(), err) } diff --git a/pkg/api/folder_permission_test.go b/pkg/api/folder_permission_test.go index bc35031ce52..552577963b4 100644 --- a/pkg/api/folder_permission_test.go +++ b/pkg/api/folder_permission_test.go @@ -139,7 +139,7 @@ func TestFolderPermissionApiEndpoint(t *testing.T) { guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ CanAdminValue: true, CheckPermissionBeforeUpdateValue: false, - CheckPermissionBeforeUpdateError: guardian.ErrGuardianDuplicatePermission, + CheckPermissionBeforeUpdateError: guardian.ErrGuardianPermissionExists, }) mock := &fakeFolderService{ @@ -175,7 +175,7 @@ func TestFolderPermissionApiEndpoint(t *testing.T) { guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ CanAdminValue: true, CheckPermissionBeforeUpdateValue: false, - CheckPermissionBeforeUpdateError: guardian.ErrGuardianOverrideLowerPresedence}, + CheckPermissionBeforeUpdateError: guardian.ErrGuardianOverride}, ) mock := &fakeFolderService{ diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go index 972e87de39b..54b5386f40d 100644 --- a/pkg/services/guardian/guardian.go +++ b/pkg/services/guardian/guardian.go @@ -10,8 +10,8 @@ import ( ) var ( - ErrGuardianDuplicatePermission = errors.New("You cannot add multiple permissions for a user, team or role") - ErrGuardianOverrideLowerPresedence = errors.New("You cannot override a permission with a lower presedence permission") + ErrGuardianPermissionExists = errors.New("This 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 @@ -133,7 +133,7 @@ func (g *dashboardGuardianImpl) CheckPermissionBeforeUpdate(permission m.Permiss if (a.UserId <= 0 && a.TeamId <= 0 && a.UserId == p.UserId && a.TeamId == p.TeamId && a.Role == p.Role) || (a.UserId > 0 && a.UserId == p.UserId) || (a.TeamId > 0 && a.TeamId == p.TeamId) { - return false, ErrGuardianDuplicatePermission + return false, ErrGuardianPermissionExists } } @@ -154,7 +154,7 @@ func (g *dashboardGuardianImpl) CheckPermissionBeforeUpdate(permission m.Permiss if (a.UserId <= 0 && a.TeamId <= 0 && a.UserId == existingPerm.UserId && a.TeamId == existingPerm.TeamId && *a.Role == *existingPerm.Role && a.Permission <= existingPerm.Permission) || (a.UserId > 0 && a.UserId == existingPerm.UserId && a.Permission <= existingPerm.Permission) || (a.TeamId > 0 && a.TeamId == existingPerm.TeamId && a.Permission <= existingPerm.Permission) { - return false, ErrGuardianOverrideLowerPresedence + return false, ErrGuardianOverride } } } diff --git a/pkg/services/guardian/guardian_test.go b/pkg/services/guardian/guardian_test.go index d420d1add97..0da14a51c5c 100644 --- a/pkg/services/guardian/guardian_test.go +++ b/pkg/services/guardian/guardian_test.go @@ -29,7 +29,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianDuplicatePermission) + So(err, ShouldEqual, ErrGuardianPermissionExists) }) Convey("With duplicate team/role permissions should return error", func() { @@ -38,7 +38,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_ADMIN}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianDuplicatePermission) + So(err, ShouldEqual, ErrGuardianPermissionExists) }) Convey("With duplicate everyone/role permissions should return error", func() { @@ -47,7 +47,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 1, Permission: m.PERMISSION_ADMIN}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianDuplicatePermission) + So(err, ShouldEqual, ErrGuardianPermissionExists) }) }) @@ -66,7 +66,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_ADMIN}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + So(err, ShouldEqual, ErrGuardianOverride) }) Convey("When trying to update dashboard permissions with edit user permission should return error", func() { @@ -74,7 +74,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_EDIT}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + So(err, ShouldEqual, ErrGuardianOverride) }) Convey("When trying to update dashboard permissions with view user permission should return error", func() { @@ -82,7 +82,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_VIEW}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + So(err, ShouldEqual, ErrGuardianOverride) }) }) @@ -109,7 +109,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_EDIT}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + So(err, ShouldEqual, ErrGuardianOverride) }) Convey("When trying to update dashboard permissions with view user permission should return error", func() { @@ -117,7 +117,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_VIEW}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + So(err, ShouldEqual, ErrGuardianOverride) }) }) @@ -152,7 +152,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_VIEW}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + So(err, ShouldEqual, ErrGuardianOverride) }) }) @@ -171,7 +171,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_ADMIN}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + So(err, ShouldEqual, ErrGuardianOverride) }) Convey("When trying to update dashboard permissions with edit team permission should return error", func() { @@ -179,7 +179,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_EDIT}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + So(err, ShouldEqual, ErrGuardianOverride) }) Convey("When trying to update dashboard permissions with view team permission should return error", func() { @@ -187,7 +187,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_VIEW}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + So(err, ShouldEqual, ErrGuardianOverride) }) }) @@ -214,7 +214,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_EDIT}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + So(err, ShouldEqual, ErrGuardianOverride) }) Convey("When trying to update dashboard permissions with view team permission should return error", func() { @@ -222,7 +222,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_VIEW}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + So(err, ShouldEqual, ErrGuardianOverride) }) }) @@ -257,7 +257,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_VIEW}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + So(err, ShouldEqual, ErrGuardianOverride) }) }) @@ -285,7 +285,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_EDIT}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + So(err, ShouldEqual, ErrGuardianOverride) }) Convey("When trying to update dashboard permissions with everyone with editor role can view permission should return error", func() { @@ -293,7 +293,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_VIEW}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + So(err, ShouldEqual, ErrGuardianOverride) }) }) @@ -329,7 +329,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_VIEW}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + So(err, ShouldEqual, ErrGuardianOverride) }) }) }) From 71a0a298bf7a5010bd9b6dfdebc692d3ea631098 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 27 Feb 2018 16:04:26 +0100 Subject: [PATCH 49/58] permissions: remove client validation and handle server validation --- .../Permissions/AddPermissions.jest.tsx | 2 +- .../components/Permissions/AddPermissions.tsx | 8 -- .../PermissionsStore/PermissionsStore.jest.ts | 75 +++---------------- .../PermissionsStore/PermissionsStore.ts | 51 +++++-------- 4 files changed, 29 insertions(+), 107 deletions(-) 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 { - {permissions.error ? ( -
- - - {permissions.error} - -
- ) : null} ); } diff --git a/public/app/stores/PermissionsStore/PermissionsStore.jest.ts b/public/app/stores/PermissionsStore/PermissionsStore.jest.ts index f7332516bef..c3bc6016e50 100644 --- a/public/app/stores/PermissionsStore/PermissionsStore.jest.ts +++ b/public/app/stores/PermissionsStore/PermissionsStore.jest.ts @@ -1,10 +1,10 @@ -import { PermissionsStore, aclTypeValues } from './PermissionsStore'; +import { PermissionsStore } from './PermissionsStore'; import { backendSrv } from 'test/mocks/common'; describe('PermissionsStore', () => { let store; - beforeEach(() => { + beforeEach(async () => { backendSrv.get.mockReturnValue( Promise.resolve([ { id: 2, dashboardId: 1, role: 'Viewer', permission: 1, permissionName: 'View' }, @@ -20,7 +20,7 @@ describe('PermissionsStore', () => { ]) ); - backendSrv.post = jest.fn(); + backendSrv.post = jest.fn(() => Promise.resolve({})); store = PermissionsStore.create( { @@ -32,14 +32,14 @@ describe('PermissionsStore', () => { } ); - return store.load(1, false, false); + await store.load(1, false, false); }); - it('should save update on permission change', () => { + it('should save update on permission change', async () => { expect(store.items[0].permission).toBe(1); expect(store.items[0].permissionName).toBe('View'); - store.updatePermissionOnIndex(0, 2, 'Edit'); + await store.updatePermissionOnIndex(0, 2, 'Edit'); expect(store.items[0].permission).toBe(2); expect(store.items[0].permissionName).toBe('Edit'); @@ -47,69 +47,18 @@ describe('PermissionsStore', () => { expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/permissions'); }); - it('should save removed permissions automatically', () => { + it('should save removed permissions automatically', async () => { expect(store.items.length).toBe(3); - store.removeStoreItem(2); + await store.removeStoreItem(2); expect(store.items.length).toBe(2); expect(backendSrv.post.mock.calls.length).toBe(1); expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/permissions'); }); - describe('when duplicate team permissions are added', () => { - beforeEach(() => { - const newItem = { - teamId: 10, - team: 'tester-team', - permission: 1, - dashboardId: 1, - }; - store.resetNewType(); - store.newItem.setTeam(newItem.teamId, newItem.team); - store.newItem.setPermission(newItem.permission); - store.addStoreItem(); - - store.newItem.setTeam(newItem.teamId, newItem.team); - store.newItem.setPermission(newItem.permission); - store.addStoreItem(); - }); - - it('should return a validation error', () => { - expect(store.items.length).toBe(4); - expect(store.error).toBe('This permission exists already.'); - expect(backendSrv.post.mock.calls.length).toBe(1); - }); - }); - - describe('when duplicate user permissions are added', () => { - beforeEach(() => { - expect(store.items.length).toBe(3); - const newItem = { - userId: 10, - userLogin: 'tester1', - permission: 1, - dashboardId: 1, - }; - store.setNewType(aclTypeValues.USER.value); - store.newItem.setUser(newItem.userId, newItem.userLogin); - store.newItem.setPermission(newItem.permission); - store.addStoreItem(); - store.setNewType(aclTypeValues.USER.value); - store.newItem.setUser(newItem.userId, newItem.userLogin); - store.newItem.setPermission(newItem.permission); - store.addStoreItem(); - }); - - it('should return a validation error', () => { - expect(store.items.length).toBe(4); - expect(store.error).toBe('This permission exists already.'); - expect(backendSrv.post.mock.calls.length).toBe(1); - }); - }); - describe('when one inherited and one not inherited team permission are added', () => { - beforeEach(() => { + beforeEach(async () => { const overridingItemForChildDashboard = { team: 'MyTestTeam', dashboardId: 1, @@ -120,11 +69,7 @@ describe('PermissionsStore', () => { store.resetNewType(); store.newItem.setTeam(overridingItemForChildDashboard.teamId, overridingItemForChildDashboard.team); store.newItem.setPermission(overridingItemForChildDashboard.permission); - store.addStoreItem(); - }); - - it('should allowing overriding the inherited permission and not throw a validation error', () => { - expect(store.error).toBe(null); + await store.addStoreItem(); }); it('should add new overriding permission', () => { diff --git a/public/app/stores/PermissionsStore/PermissionsStore.ts b/public/app/stores/PermissionsStore/PermissionsStore.ts index 88b26a2886f..79df593f06e 100644 --- a/public/app/stores/PermissionsStore/PermissionsStore.ts +++ b/public/app/stores/PermissionsStore/PermissionsStore.ts @@ -1,8 +1,6 @@ import { types, getEnv, flow } from 'mobx-state-tree'; import { PermissionsStoreItem } from './PermissionsStoreItem'; -const duplicateError = 'This permission exists already.'; - export const permissionOptions = [ { value: 1, label: 'View', description: 'Can view dashboards.' }, { value: 2, label: 'Edit', description: 'Can add, edit and delete dashboards.' }, @@ -75,7 +73,6 @@ export const PermissionsStore = types isFolder: types.maybe(types.boolean), dashboardId: types.maybe(types.number), items: types.optional(types.array(PermissionsStoreItem), []), - error: types.maybe(types.string), originalItems: types.optional(types.array(PermissionsStoreItem), []), newType: types.optional(types.string, defaultNewType), newItem: types.maybe(NewPermissionsItem), @@ -88,7 +85,6 @@ export const PermissionsStore = types return isDuplicate(it, item); }); if (dupe) { - self.error = duplicateError; return false; } @@ -96,8 +92,7 @@ export const PermissionsStore = types }, })) .actions(self => { - const resetNewType = () => { - self.error = null; + const resetNewTypeInternal = () => { self.newItem = NewPermissionsItem.create(); }; @@ -115,11 +110,9 @@ export const PermissionsStore = types self.items = items; self.originalItems = items; self.fetching = false; - self.error = null; }), addStoreItem: flow(function* addStoreItem() { - self.error = null; let item = { type: self.newItem.type, permission: self.newItem.permission, @@ -147,19 +140,21 @@ export const PermissionsStore = types throw Error('Unknown type: ' + self.newItem.type); } - if (!self.isValid(item)) { - return undefined; - } + const updatedItems = self.items.peek(); + const newItem = prepareItem(item, self.dashboardId, self.isFolder, self.isInRoot); + updatedItems.push(newItem); - self.items.push(prepareItem(item, self.dashboardId, self.isFolder, self.isInRoot)); - resetNewType(); - return updateItems(self); + try { + yield updateItems(self, updatedItems); + self.items.push(newItem); + resetNewTypeInternal(); + } catch {} + yield Promise.resolve(); }), removeStoreItem: flow(function* removeStoreItem(idx: number) { - self.error = null; self.items.splice(idx, 1); - return updateItems(self); + yield updateItems(self, self.items.peek()); }), updatePermissionOnIndex: flow(function* updatePermissionOnIndex( @@ -167,9 +162,8 @@ export const PermissionsStore = types permission: number, permissionName: string ) { - self.error = null; self.items[idx].updatePermission(permission, permissionName); - return updateItems(self); + yield updateItems(self, self.items.peek()); }), setNewType(newType: string) { @@ -177,7 +171,7 @@ export const PermissionsStore = types }, resetNewType() { - resetNewType(); + resetNewTypeInternal(); }, toggleAddPermissions() { @@ -190,12 +184,10 @@ export const PermissionsStore = types }; }); -const updateItems = self => { - self.error = null; - +const updateItems = (self, items) => { const backendSrv = getEnv(self).backendSrv; const updated = []; - for (let item of self.items) { + for (let item of items) { if (item.inherited) { continue; } @@ -208,16 +200,9 @@ const updateItems = self => { }); } - let res; - try { - res = backendSrv.post(`/api/dashboards/id/${self.dashboardId}/permissions`, { - items: updated, - }); - } catch (error) { - self.error = error; - } - - return res; + return backendSrv.post(`/api/dashboards/id/${self.dashboardId}/permissions`, { + items: updated, + }); }; const prepareServerResponse = (response, dashboardId: number, isFolder: boolean, isInRoot: boolean) => { From ecf438f8d01a521c63129164783fd3c77abafd73 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 27 Feb 2018 17:53:30 +0100 Subject: [PATCH 50/58] dashboard: add permission check for diff api route ref #10770 --- pkg/api/dashboard.go | 16 +++++++-- pkg/api/dashboard_test.go | 69 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) 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_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) From f44e476580d7b4fe0ca06be802dde4689131820e Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 28 Feb 2018 08:48:28 +0100 Subject: [PATCH 51/58] permissions: fix validation of permissions before update Did a bad pointer comparison so extended the tests for duplicate permissions. --- pkg/models/dashboard_acl.go | 21 +++++++++ pkg/services/guardian/guardian.go | 33 ++++++++++---- pkg/services/guardian/guardian_test.go | 62 +++++++++++++++++++++++--- 3 files changed, 103 insertions(+), 13 deletions(-) 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/guardian/guardian.go b/pkg/services/guardian/guardian.go index 54b5386f40d..811b38cac86 100644 --- a/pkg/services/guardian/guardian.go +++ b/pkg/services/guardian/guardian.go @@ -10,7 +10,7 @@ import ( ) var ( - ErrGuardianPermissionExists = errors.New("This permission already exists") + ErrGuardianPermissionExists = errors.New("Permission already exists") ErrGuardianOverride = errors.New("You can only override a permission to be higher") ) @@ -127,17 +127,23 @@ func (g *dashboardGuardianImpl) checkAcl(permission m.PermissionType, acl []*m.D func (g *dashboardGuardianImpl) CheckPermissionBeforeUpdate(permission m.PermissionType, updatePermissions []*m.DashboardAcl) (bool, error) { 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.UserId <= 0 && a.TeamId <= 0 && a.UserId == p.UserId && a.TeamId == p.TeamId && a.Role == p.Role) || - (a.UserId > 0 && a.UserId == p.UserId) || - (a.TeamId > 0 && a.TeamId == p.TeamId) { + if a.IsDuplicateOf(aclItem) { return false, ErrGuardianPermissionExists } } - acl = append(acl, &m.DashboardAclInfoDTO{DashboardId: p.DashboardId, UserId: p.UserId, TeamId: p.TeamId, Role: p.Role, Permission: p.Permission}) + acl = append(acl, aclItem) } existingPermissions, err := g.GetAcl() @@ -145,15 +151,19 @@ func (g *dashboardGuardianImpl) CheckPermissionBeforeUpdate(permission m.Permiss return false, err } + // 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.UserId <= 0 && a.TeamId <= 0 && a.UserId == existingPerm.UserId && a.TeamId == existingPerm.TeamId && *a.Role == *existingPerm.Role && a.Permission <= existingPerm.Permission) || - (a.UserId > 0 && a.UserId == existingPerm.UserId && a.Permission <= existingPerm.Permission) || - (a.TeamId > 0 && a.TeamId == existingPerm.TeamId && a.Permission <= existingPerm.Permission) { + if a.IsDuplicateOf(existingPerm) && a.Permission <= existingPerm.Permission { return false, ErrGuardianOverride } } @@ -177,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 } diff --git a/pkg/services/guardian/guardian_test.go b/pkg/services/guardian/guardian_test.go index 0da14a51c5c..bb7e6bd1a72 100644 --- a/pkg/services/guardian/guardian_test.go +++ b/pkg/services/guardian/guardian_test.go @@ -23,7 +23,7 @@ func TestGuardian(t *testing.T) { So(canView, ShouldBeTrue) Convey("When trying to update permissions", func() { - Convey("With duplicate user/role permissions should return error", 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}, @@ -32,7 +32,7 @@ func TestGuardian(t *testing.T) { So(err, ShouldEqual, ErrGuardianPermissionExists) }) - Convey("With duplicate team/role permissions should return error", func() { + 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}, @@ -41,14 +41,66 @@ func TestGuardian(t *testing.T) { So(err, ShouldEqual, ErrGuardianPermissionExists) }) - Convey("With duplicate everyone/role permissions should return error", func() { + Convey("With duplicate everyone with editor role permission should return error", func() { + r := m.ROLE_EDITOR p := []*m.DashboardAcl{ - {OrgId: 1, DashboardId: 1, Permission: m.PERMISSION_VIEW}, - {OrgId: 1, DashboardId: 1, Permission: m.PERMISSION_ADMIN}, + {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() { From 576deb37e8e309738cb7af7877335ac7bc765180 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 28 Feb 2018 13:01:35 +0100 Subject: [PATCH 52/58] login: hide sign up if configured so. Fixes #11041 --- public/app/partials/login.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/partials/login.html b/public/app/partials/login.html index 54c59c04429..8680924977f 100644 --- a/public/app/partials/login.html +++ b/public/app/partials/login.html @@ -59,7 +59,7 @@ Sign in with {{oauth.generic_oauth.name}} -