diff --git a/.circleci/config.yml b/.circleci/config.yml index 60b3ae91ccc..b173dbff481 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -335,6 +335,9 @@ jobs: - run: name: deploy to gcp command: '/opt/google-cloud-sdk/bin/gsutil cp ./enterprise-dist/* gs://$GCP_BUCKET_NAME/enterprise/master' + - run: + name: Deploy to grafana.com + command: 'cd enterprise-dist && ../scripts/build/release_publisher/release_publisher -apikey ${GRAFANA_COM_API_KEY} -enterprise -from-local' deploy-enterprise-release: @@ -403,7 +406,7 @@ jobs: command: '/opt/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file=/tmp/gcpkey.json' - run: name: deploy to gcp - command: '/opt/google-cloud-sdk/bin/gsutil cp ./dist/* gs://R/oss/release' + command: '/opt/google-cloud-sdk/bin/gsutil cp ./dist/* gs://$GCP_BUCKET_NAME/oss/release' - run: name: Deploy to Grafana.com command: './scripts/build/publish.sh' diff --git a/CHANGELOG.md b/CHANGELOG.md index 97537ec34f7..2461faf8cac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,12 +12,15 @@ ### Minor * **Cloudwatch**: Show all available CloudWatch regions [#12308](https://github.com/grafana/grafana/issues/12308), thx [@mtanda](https://github.com/mtanda) +* **Cloudwatch**: AWS/Connect metrics and dimensions [#13970](https://github.com/grafana/grafana/pull/13970), thx [@zcoffy](https://github.com/zcoffy) * **Postgres**: Add delta window function to postgres query builder [#13925](https://github.com/grafana/grafana/issues/13925), thx [svenklemm](https://github.com/svenklemm) * **Units**: New clock time format, to format ms or second values as for example `01h:59m`, [#13635](https://github.com/grafana/grafana/issues/13635), thx [@franciscocpg](https://github.com/franciscocpg) * **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu) * **DingDing**: Can't receive DingDing alert when alert is triggered [#13723](https://github.com/grafana/grafana/issues/13723), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino) * **Internal metrics**: Renamed `grafana_info` to `grafana_build_info` and added branch, goversion and revision [#13876](https://github.com/grafana/grafana/pull/13876) * **Alerting**: Increaste default duration for queries [#13945](https://github.com/grafana/grafana/pull/13945) +* **Table**: Fix CSS alpha background-color applied twice in table cell with link [#13606](https://github.com/grafana/grafana/issues/13606), thx [@grisme](https://github.com/grisme) +* **Alerting**: More options for the Slack Alert notifier [#13993](https://github.com/grafana/grafana/issues/13993), thx [@andreykaipov](https://github.com/andreykaipov) ### Breaking changes @@ -25,7 +28,10 @@ # 5.3.3 (unreleased) +* **Alerting**: Delete alerts when parent folder was deleted [#13322](https://github.com/grafana/grafana/issues/13322) * **MySQL**: Fix `$__timeFilter()` should respect local time zone [#13769](https://github.com/grafana/grafana/issues/13769) +* **Dashboard**: Fix datasource selection in panel by enter key [#13932](https://github.com/grafana/grafana/issues/13932) +* **Graph**: Fix table legend height when positioned below graph and using Internet Explorer 11 [#13903](https://github.com/grafana/grafana/issues/13903) # 5.3.2 (2018-10-24) diff --git a/build.go b/build.go index b136754efbc..a2a1fb825d9 100644 --- a/build.go +++ b/build.go @@ -41,8 +41,8 @@ var ( race bool phjsToRelease string workingDir string - includeBuildNumber bool = true - buildNumber int = 0 + includeBuildId bool = true + buildId string = "0" binaries []string = []string{"grafana-server", "grafana-cli"} isDev bool = false enterprise bool = false @@ -54,6 +54,8 @@ func main() { ensureGoPath() + var buildIdRaw string + flag.StringVar(&goarch, "goarch", runtime.GOARCH, "GOARCH") flag.StringVar(&goos, "goos", runtime.GOOS, "GOOS") flag.StringVar(&gocc, "cc", "", "CC") @@ -61,12 +63,14 @@ func main() { flag.StringVar(&pkgArch, "pkg-arch", "", "PKG ARCH") flag.StringVar(&phjsToRelease, "phjs", "", "PhantomJS binary") flag.BoolVar(&race, "race", race, "Use race detector") - flag.BoolVar(&includeBuildNumber, "includeBuildNumber", includeBuildNumber, "IncludeBuildNumber in package name") + flag.BoolVar(&includeBuildId, "includeBuildId", includeBuildId, "IncludeBuildId in package name") flag.BoolVar(&enterprise, "enterprise", enterprise, "Build enterprise version of Grafana") - flag.IntVar(&buildNumber, "buildNumber", 0, "Build number from CI system") + flag.StringVar(&buildIdRaw, "buildId", "0", "Build ID from CI system") flag.BoolVar(&isDev, "dev", isDev, "optimal for development, skips certain steps") flag.Parse() + buildId = shortenBuildId(buildIdRaw) + readVersionFromPackageJson() if pkgArch == "" { @@ -197,9 +201,9 @@ func readVersionFromPackageJson() { } // add timestamp to iteration - if includeBuildNumber { - if buildNumber != 0 { - linuxPackageIteration = fmt.Sprintf("%d%s", buildNumber, linuxPackageIteration) + if includeBuildId { + if buildId != "0" { + linuxPackageIteration = fmt.Sprintf("%s%s", buildId, linuxPackageIteration) } else { linuxPackageIteration = fmt.Sprintf("%d%s", time.Now().Unix(), linuxPackageIteration) } @@ -392,7 +396,7 @@ func grunt(params ...string) { func gruntBuildArg(task string) []string { args := []string{task} - if includeBuildNumber { + if includeBuildId { args = append(args, fmt.Sprintf("--pkgVer=%v-%v", linuxPackageVersion, linuxPackageIteration)) } else { args = append(args, fmt.Sprintf("--pkgVer=%v", version)) @@ -632,3 +636,11 @@ func shaFile(file string) error { return out.Close() } + +func shortenBuildId(buildId string) string { + buildId = strings.Replace(buildId, "-", "", -1) + if (len(buildId) < 9) { + return buildId + } + return buildId[0:8] +} diff --git a/conf/defaults.ini b/conf/defaults.ini index 481bb002582..679a6a88eb7 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -344,6 +344,7 @@ header_property = username auto_sign_up = true ldap_sync_ttl = 60 whitelist = +headers = #################################### Auth LDAP ########################### [auth.ldap] diff --git a/conf/sample.ini b/conf/sample.ini index 61eb1d695e8..c6b716a731d 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -294,6 +294,7 @@ log_queries = ;auto_sign_up = true ;ldap_sync_ttl = 60 ;whitelist = 192.168.1.1, 192.168.2.1 +;headers = Email:X-User-Email, Name:X-User-Name #################################### Basic Auth ########################## [auth.basic] diff --git a/devenv/dev-dashboards/panel_tests_table.json b/devenv/dev-dashboards/panel_tests_table.json index 8337e9cd746..ff0288c340a 100644 --- a/devenv/dev-dashboards/panel_tests_table.json +++ b/devenv/dev-dashboards/panel_tests_table.json @@ -404,6 +404,112 @@ "title": "Column style thresholds & units", "transform": "timeseries_to_columns", "type": "table" + }, + { + "columns": [], + "datasource": "gdev-testdata", + "fontSize": "100%", + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 26 + }, + "id": 6, + "links": [], + "pageSize": 20, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "colorMode": "cell", + "colors": [ + "rgba(245, 54, 54, 0.5)", + "rgba(237, 129, 40, 0.5)", + "rgba(50, 172, 45, 0.5)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "link": true, + "linkTargetBlank": true, + "linkTooltip": "", + "linkUrl": "http://www.grafana.com", + "mappingType": 1, + "pattern": "ColorCell", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "currencyUSD" + }, + { + "alias": "", + "colorMode": "value", + "colors": [ + "rgba(245, 54, 54, 0.5)", + "rgba(237, 129, 40, 0.5)", + "rgba(50, 172, 45, 0.5)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "link": true, + "linkUrl": "http://www.grafana.com", + "mappingType": 1, + "pattern": "ColorValue", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "Bps" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "ColorValue", + "expr": "", + "format": "table", + "intervalFactor": 1, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "null,1,20,90,30,5,0,20,10" + }, + { + "alias": "ColorCell", + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "null,5,1,2,3,4,5,10,20" + } + ], + "title": "Column style thresholds and links", + "transform": "timeseries_to_columns", + "type": "table" } ], "refresh": false, @@ -449,5 +555,5 @@ "timezone": "browser", "title": "Panel Tests - Table", "uid": "pttable", - "version": 1 -} + "version": 2 +} \ No newline at end of file diff --git a/docs/sources/administration/permissions.md b/docs/sources/administration/permissions.md deleted file mode 100644 index 0d374f03647..00000000000 --- a/docs/sources/administration/permissions.md +++ /dev/null @@ -1,116 +0,0 @@ -+++ -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 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 permissions 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 dashboards/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 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. - -- **View**: Can only view existing dashboards/folders. -- 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. - -### Data source permissions - -Permissions on dashboards and folders **do not** include permissions on data sources. A user with `Viewer` role -can still issue any possible query to a data source, not just those queries that exist on dashboards he/she has access to. -We hope to add permissions on data sources in a future release. Until then **do not** view dashboard permissions as a secure -way to restrict user data access. Dashboard permissions only limits what dashboards & folders a user can view & edit not which -data sources a user can access nor what queries a user can issue. - diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index 9149aa42130..60e89b486a5 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -156,7 +156,7 @@ Since not all datasources have the same configuration settings we only have the | tlsSkipVerify | boolean | *All* | Controls whether a client verifies the server's certificate chain and host name. | | graphiteVersion | string | Graphite | Graphite version | | timeInterval | string | Prometheus, Elasticsearch, InfluxDB, MySQL, PostgreSQL & MSSQL | Lowest interval/step value that should be used for this data source | -| esVersion | number | Elasticsearch | Elasticsearch version as a number (2/5/56) | +| esVersion | number | Elasticsearch | Elasticsearch version as a number (2/5/56/60) | | timeField | string | Elasticsearch | Which field that should be used as timestamp | | interval | string | Elasticsearch | Index date time format. nil(No Pattern), 'Hourly', 'Daily', 'Weekly', 'Monthly' or 'Yearly' | | authType | string | Cloudwatch | Auth provider. keys/credentials/arn | diff --git a/docs/sources/auth/enhanced_ldap.md b/docs/sources/auth/enhanced_ldap.md new file mode 100644 index 00000000000..8eec57b1429 --- /dev/null +++ b/docs/sources/auth/enhanced_ldap.md @@ -0,0 +1,43 @@ ++++ +title = "Enhanced LDAP Integration" +description = "Grafana Enhanced LDAP Integration Guide " +keywords = ["grafana", "configuration", "documentation", "ldap", "active directory", "enterprise"] +type = "docs" +[menu.docs] +name = "Enhanced LDAP" +identifier = "enhanced-ldap" +parent = "authentication" +weight = 3 ++++ + +# Enhanced LDAP Integration + +> Enhanced LDAP Integration is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "enterprise/index.md" >}}). + +The enhanced LDAP integration adds additional functionality on top of the [existing LDAP integration]({{< relref "auth/ldap.md" >}}). + +## LDAP Group Synchronization for Teams + +{{< docs-imagebox img="/img/docs/enterprise/team_members_ldap.png" class="docs-image--no-shadow docs-image--right" max-width= "600px" >}} + +With the enhanced LDAP integration it's possible to setup synchronization between LDAP groups and teams. This enables LDAP users which are members +of certain LDAP groups to automatically be added/removed as members to certain teams in Grafana. Currently the synchronization will only happen every +time a user logs in, but an active background synchronization is currently being developed. + +Grafana keeps track of all synchronized users in teams and you can see which users have been synchronized from LDAP in the team members list, see `LDAP` label in screenshot. +This mechanism allows Grafana to remove an existing synchronized user from a team when its LDAP group membership changes. This mechanism also enables you to manually add +a user as member of a team and it will not be removed when the user signs in. This gives you flexibility to combine LDAP group memberships and Grafana team memberships. + +
+ +### Enable LDAP group synchronization for a team + +{{< docs-imagebox img="/img/docs/enterprise/team_add_external_group.png" class="docs-image--no-shadow docs-image--right" max-width= "600px" >}} + +1. Navigate to Configuration / Teams. +2. Select a team. +3. Select the External group sync tab and click on the `Add group` button. +4. Insert LDAP distinguished name (DN) of LDAP group you want to synchronize with the team. +5. Click on `Add group` button to save. + + diff --git a/docs/sources/enterprise/index.md b/docs/sources/enterprise/index.md new file mode 100644 index 00000000000..f65fa55f02b --- /dev/null +++ b/docs/sources/enterprise/index.md @@ -0,0 +1,67 @@ ++++ +title = "Grafana Enterprise" +description = "Grafana Enterprise overview" +keywords = ["grafana", "documentation", "datasource", "permissions", "ldap", "licensing", "enterprise"] +type = "docs" +[menu.docs] +name = "Grafana Enterprise" +identifier = "enterprise" +weight = 30 ++++ + +# Grafana Enterprise + +Grafana Enterprise is a commercial edition of Grafana that includes additional features not found in the open source +version. + +Building on everything you already know and love about Grafana, Grafana Enterprise adds premium data sources, +advanced authentication options, more permission controls, 24x7x365 support, and training from the core Grafana team. + +Grafana Enterprise includes all of the features found in the open source edition and more. + +___ + +### Enhanced LDAP Integration + +With Grafana Enterprise you can set up synchronization between LDAP Groups and Teams. [Learn More]({{< relref "auth/enhanced_ldap.md" >}}). + +### Datasource Permissions + +Datasource permissions allow you to restrict query access to only specific Teams and Users. [Learn More]({{< relref "permissions/datasource_permissions.md" >}}). + +### Premium Plugins + +With a Grafana Enterprise licence you will get access to premium plugins, including: + +* [Splunk](https://grafana.com/plugins/grafana-splunk-datasource) +* [AppDynamics](https://grafana.com/plugins/dlopes7-appdynamics-datasource) +* [DataDog](https://grafana.com/plugins/grafana-datadog-datasource) +* [Dynatrace](https://grafana.com/plugins/grafana-dynatrace-datasource) +* [New Relic](https://grafana.com/plugins/grafana-newrelic-datasource) + +## Try Grafana Enterprise + +You can learn more about Grafana Enterprise [here](https://grafana.com/enterprise). To purchase or obtain a trial license contact +the Grafana Labs [Sales Team](https://grafana.com/contact?about=support&topic=Grafana%20Enterprise). + +## License file management + +To download your Grafana Enterprise license log in to your [Grafana.com](https://grafana.com) account and go to your **Org +Profile**. In the side menu there is a section for Grafana Enterprise licenses. At the bottom of the license +details page there is **Download Token** link that will download the *license.jwt* file containing your license. + +Place the *license.jwt* file in Grafana's data folder. This is usually located at `/var/lib/grafana/data` on linux systems. + +You can also configure a custom location for the license file via the ini setting: + +```bash +[enterprise] +license_path = /company/secrets/license.jwt +``` + +This setting can also be set via ENV variable which is useful if you're running Grafana via docker and have a custom +volume where you have placed the license file. In this case set the ENV variable `GF_ENTERPRISE_LICENSE_PATH` to point +to the location of your license file. + + + diff --git a/docs/sources/features/datasources/cloudwatch.md b/docs/sources/features/datasources/cloudwatch.md index be36d108475..e2bcb50bb1d 100644 --- a/docs/sources/features/datasources/cloudwatch.md +++ b/docs/sources/features/datasources/cloudwatch.md @@ -60,7 +60,8 @@ Here is a minimal policy example: "Effect": "Allow", "Action": [ "cloudwatch:ListMetrics", - "cloudwatch:GetMetricStatistics" + "cloudwatch:GetMetricStatistics", + "cloudwatch:GetMetricData" ], "Resource": "*" }, diff --git a/docs/sources/features/datasources/elasticsearch.md b/docs/sources/features/datasources/elasticsearch.md index 80a2f9a828a..aa60eb7cbc1 100644 --- a/docs/sources/features/datasources/elasticsearch.md +++ b/docs/sources/features/datasources/elasticsearch.md @@ -59,7 +59,7 @@ a time pattern for the index name or a wildcard. ### Elasticsearch version Be sure to specify your Elasticsearch version in the version selection dropdown. This is very important as there are differences how queries are composed. -Currently the versions available is 2.x, 5.x and 5.6+ where 5.6+ means a version of 5.6 or higher, 6.3.2 for example. +Currently the versions available is 2.x, 5.x, 5.6+ or 6.0+. 5.6+ means a version of 5.6 or less than 6.0. 6.0+ means a version of 6.0 or higher, 6.3.2 for example. ### Min time interval A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example `1m` if your data is written every minute. diff --git a/docs/sources/guides/whats-new-in-v5-3.md b/docs/sources/guides/whats-new-in-v5-3.md index 5dcadc0813d..10592f51648 100644 --- a/docs/sources/guides/whats-new-in-v5-3.md +++ b/docs/sources/guides/whats-new-in-v5-3.md @@ -18,7 +18,7 @@ Grafana v5.3 brings new features, many enhancements and bug fixes. This article - [TV mode]({{< relref "#tv-and-kiosk-mode" >}}) is improved and more accessible - [Alerting]({{< relref "#notification-reminders" >}}) with notification reminders - [Postgres]({{< relref "#postgres-query-builder" >}}) gets a new query builder! -- [OAuth]({{< relref "#improved-oauth-support-for-gitlab" >}}) support for Gitlab is improved +- [OAuth]({{< relref "#improved-oauth-support-for-gitlab" >}}) support for GitLab is improved - [Annotations]({{< relref "#annotations" >}}) with template variable filtering - [Variables]({{< relref "#variables" >}}) with free text support @@ -69,9 +69,9 @@ Grafana 5.3 comes with a new graphical query builder for Postgres. This brings P {{< docs-imagebox img="/img/docs/v53/postgres_query_still.png" class="docs-image--no-shadow" animated-gif="/img/docs/v53/postgres_query.gif" >}} -## Improved OAuth Support for Gitlab +## Improved OAuth Support for GitLab -Grafana 5.3 comes with a new OAuth integration for Gitlab that enables configuration to only allow users that are a member of certain Gitlab groups to authenticate. This makes it possible to use Gitlab OAuth with Grafana in a shared environment without giving everyone access to Grafana. +Grafana 5.3 comes with a new OAuth integration for GitLab that enables configuration to only allow users that are a member of certain GitLab groups to authenticate. This makes it possible to use GitLab OAuth with Grafana in a shared environment without giving everyone access to Grafana. Learn how to enable and configure it in the [documentation](/auth/gitlab/). ## Annotations diff --git a/docs/sources/http_api/datasource_permissions.md b/docs/sources/http_api/datasource_permissions.md new file mode 100644 index 00000000000..226beac3728 --- /dev/null +++ b/docs/sources/http_api/datasource_permissions.md @@ -0,0 +1,249 @@ ++++ +title = "Datasource Permissions HTTP API " +description = "Grafana Datasource Permissions HTTP API" +keywords = ["grafana", "http", "documentation", "api", "datasource", "permission", "permissions", "acl", "enterprise"] +aliases = ["/http_api/datasourcepermissions/"] +type = "docs" +[menu.docs] +name = "Datasource Permissions" +parent = "http_api" ++++ + +# Datasource Permissions API + +> Datasource Permissions is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "enterprise/index.md" >}}). + +This API can be used to enable, disable, list, add and remove permissions for a datasource. + +Permissions can be set for a user or a team. Permissions cannot be set for Admins - they always have access to everything. + +The permission levels for the permission field: + +- 1 = Query + +## Enable permissions for a datasource + +`POST /api/datasources/:id/enable-permissions` + +Enables permissions for the datasource with the given `id`. No one except Org Admins will be able to query the datasource until permissions have been added which permit certain users or teams to query the datasource. + +**Example request**: + +```http +POST /api/datasources/1/enable-permissions +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: 35 + +{"message":"Datasource permissions enabled"} +``` + +Status Codes: + +- **200** - Ok +- **400** - Permissions cannot be enabled, see response body for details +- **401** - Unauthorized +- **403** - Access denied +- **404** - Datasource not found + +## Disable permissions for a datasource + +`POST /api/datasources/:id/disable-permissions` + +Disables permissions for the datasource with the given `id`. All existing permissions will be removed and anyone will be able to query the datasource. + +**Example request**: + +```http +POST /api/datasources/1/disable-permissions +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: 35 + +{"message":"Datasource permissions disabled"} +``` + +Status Codes: + +- **200** - Ok +- **400** - Permissions cannot be disabled, see response body for details +- **401** - Unauthorized +- **403** - Access denied +- **404** - Datasource not found + +## Get permissions for a datasource + +`GET /api/datasources/:id/permissions` + +Gets all existing permissions for the datasource with the given `id`. + +**Example request**: + +```http +GET /api/datasources/1/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 + +{ + "datasourceId": 1, + "enabled": true, + "permissions": + [ + { + "id": 1, + "datasourceId": 1, + "userId": 1, + "userLogin": "user", + "userEmail": "user@test.com", + "userAvatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", + "permission": 1, + "permissionName": "Query", + "created": "2017-06-20T02:00:00+02:00", + "updated": "2017-06-20T02:00:00+02:00", + }, + { + "id": 2, + "datasourceId": 1, + "teamId": 1, + "team": "A Team", + "teamAvatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", + "permission": 1, + "permissionName": "Query", + "created": "2017-06-20T02:00:00+02:00", + "updated": "2017-06-20T02:00:00+02:00", + } + ] +} +``` + +Status Codes: + +- **200** - Ok +- **401** - Unauthorized +- **403** - Access denied +- **404** - Datasource not found + +## Add permission for a datasource + +`POST /api/datasources/:id/permissions` + +Adds a user permission for the datasource with the given `id`. + +**Example request**: + +```http +POST /api/datasources/1/permissions +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +{ + "userId": 1, + "permission": 1 +} +``` + +**Example response**: + +```http +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Content-Length: 35 + +{"message":"Datasource permission added"} +``` + +Adds a team permission for the datasource with the given `id`. + +**Example request**: + +```http +POST /api/datasources/1/permissions +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +{ + "teamId": 1, + "permission": 1 +} +``` + +**Example response**: + +```http +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Content-Length: 35 + +{"message":"Datasource permission added"} +``` + +Status Codes: + +- **200** - Ok +- **400** - Permission cannot be added, see response body for details +- **401** - Unauthorized +- **403** - Access denied +- **404** - Datasource not found + +## Remove permission for a datasource + +`DELETE /api/datasources/:id/permissions/:permissionId` + +Removes the permission with the given `permissionId` for the datasource with the given `id`. + +**Example request**: + +```http +DELETE /api/datasources/1/permissions/2 +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: 35 + +{"message":"Datasource permission removed"} +``` + +Status Codes: + +- **200** - Ok +- **401** - Unauthorized +- **403** - Access denied +- **404** - Datasource not found or permission not found diff --git a/docs/sources/http_api/external_group_sync.md b/docs/sources/http_api/external_group_sync.md new file mode 100644 index 00000000000..2ce06c2c94e --- /dev/null +++ b/docs/sources/http_api/external_group_sync.md @@ -0,0 +1,111 @@ ++++ +title = "External Group Sync HTTP API " +description = "Grafana External Group Sync HTTP API" +keywords = ["grafana", "http", "documentation", "api", "team", "teams", "group", "member", "enterprise"] +aliases = ["/http_api/external_group_sync/"] +type = "docs" +[menu.docs] +name = "External Group Sync" +parent = "http_api" ++++ + +# External Group Synchronization API + +> External Group Synchronization is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "enterprise/index.md" >}}). + +## Get External Groups + +`GET /api/teams/:teamId/groups` + +**Example Request**: + +```http +GET /api/teams/1/groups 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, + "groupId": "cn=editors,ou=groups,dc=grafana,dc=org" + } +] +``` + +Status Codes: + +- **200** - Ok +- **401** - Unauthorized +- **403** - Permission denied + +## Add External Group + +`POST /api/teams/:teamId/groups` + +**Example Request**: + +```http +POST /api/teams/1/members HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Basic YWRtaW46YWRtaW4= + +{ + "groupId": "cn=editors,ou=groups,dc=grafana,dc=org" +} +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +{"message":"Group added to Team"} +``` + +Status Codes: + +- **200** - Ok +- **400** - Group is already added to this team +- **401** - Unauthorized +- **403** - Permission denied +- **404** - Team not found + +## Remove External Group + +`DELETE /api/teams/:teamId/groups/:groupId` + +**Example Request**: + +```http +DELETE /api/teams/1/groups/cn=editors,ou=groups,dc=grafana,dc=org 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 Group removed"} +``` + +Status Codes: + +- **200** - Ok +- **401** - Unauthorized +- **403** - Permission denied +- **404** - Team not found/Group not found diff --git a/docs/sources/permissions/dashboard_folder_permissions.md b/docs/sources/permissions/dashboard_folder_permissions.md new file mode 100644 index 00000000000..83cb0ee86a3 --- /dev/null +++ b/docs/sources/permissions/dashboard_folder_permissions.md @@ -0,0 +1,73 @@ ++++ +title = "Dashboard & Folder Permissions" +description = "Grafana Dashboard & Folder Permissions Guide " +keywords = ["grafana", "configuration", "documentation", "dashboard", "folder", "permissions", "teams"] +type = "docs" +[menu.docs] +name = "Dashboard & Folder" +identifier = "dashboard-folder-permissions" +parent = "permissions" +weight = 3 ++++ + +# 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 permissions for Editors and Viewers. On this page 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 dashboards/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 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. + +## Summary + +- **View**: Can only view existing dashboards/folders. +- 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 **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/permissions/datasource_permissions.md b/docs/sources/permissions/datasource_permissions.md new file mode 100644 index 00000000000..ec54c1fbccd --- /dev/null +++ b/docs/sources/permissions/datasource_permissions.md @@ -0,0 +1,71 @@ ++++ +title = "Datasource Permissions" +description = "Grafana Datasource Permissions Guide " +keywords = ["grafana", "configuration", "documentation", "datasource", "permissions", "users", "teams", "enterprise"] +type = "docs" +[menu.docs] +name = "Datasource" +identifier = "datasource-permissions" +parent = "permissions" +weight = 4 ++++ + +# Datasource Permissions + +> Datasource Permissions is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "enterprise/index.md" >}}). + +Datasource permissions allows you to restrict access for users to query a datasource. For each datasource there is +a permission page that makes it possible to enable permissions and restrict query permissions to specific +**Users** and **Teams**. + +## Restricting Access - Enable Permissions + +{{< docs-imagebox img="/img/docs/enterprise/datasource_permissions_enable_still.png" class="docs-image--no-shadow docs-image--right" max-width= "600px" animated-gif="/img/docs/enterprise/datasource_permissions_enable.gif" >}} + +By default, permissions are disabled for datasources and a datasource in an organization can be queried by any user in +that organization. For example a user with `Viewer` role can still issue any possible query to a datasource, not just +those queries that exist on dashboards he/she has access to. + +When permissions are enabled for a datasource in an organization you will restrict admin and query access for that +datasource to [admin users](/permissions/organization_roles/#admin-role) in that organization. + +**To enable permissions for a datasource:** + +1. Navigate to Configuration / Data Sources. +2. Select the datasource you want to enable permissions for. +3. Select the Permissions tab and click on the `Enable` button. + + + +## Allow users and teams to query a datasource + +{{< docs-imagebox img="/img/docs/enterprise/datasource_permissions_add_still.png" class="docs-image--no-shadow docs-image--right" max-width= "600px" animated-gif="/img/docs/enterprise/datasource_permissions_add.gif" >}} + +After you have [enabled permissions](#restricting-access-enable-permissions) for a datasource you can assign query +permissions to users and teams which will allow access to query the datasource. + +**Assign query permission to users and teams:** + +1. Navigate to Configuration / Data Sources. +2. Select the datasource you want to assign query permissions for. +3. Select the Permissions tab. +4. click on the `Add Permission` button. +5. Select Team/User and find the team/user you want to allow query access and click on the `Save` button. + + + +## Restore Default Access - Disable Permissions + +{{< docs-imagebox img="/img/docs/enterprise/datasource_permissions_disable_still.png" class="docs-image--no-shadow docs-image--right" max-width= "600px" animated-gif="/img/docs/enterprise/datasource_permissions_disable.gif" >}} + +If you have enabled permissions for a datasource and want to return datasource permissions to the default, i.e. +datasource can be queried by any user in that organization, you can disable permissions with a click of a button. +Note that all existing permissions created for datasource will be deleted. + +**To disable permissions for a datasource:** + +1. Navigate to Configuration / Data Sources. +2. Select the datasource you want to disable permissions for. +3. Select the Permissions tab and click on the `Disable Permissions` button. + + diff --git a/docs/sources/permissions/index.md b/docs/sources/permissions/index.md new file mode 100644 index 00000000000..42514f76baf --- /dev/null +++ b/docs/sources/permissions/index.md @@ -0,0 +1,12 @@ ++++ +title = "Permissions" +description = "Permissions" +type = "docs" +[menu.docs] +name = "Permissions" +identifier = "permissions" +parent = "admin" +weight = 3 ++++ + + diff --git a/docs/sources/permissions/organization_roles.md b/docs/sources/permissions/organization_roles.md new file mode 100644 index 00000000000..626d79fad87 --- /dev/null +++ b/docs/sources/permissions/organization_roles.md @@ -0,0 +1,38 @@ ++++ +title = "Organization Roles" +description = "Grafana Organization Roles Guide " +keywords = ["grafana", "configuration", "documentation", "organization", "roles", "permissions"] +type = "docs" +[menu.docs] +name = "Organization Roles" +identifier = "organization-roles" +parent = "permissions" +weight = 2 ++++ + +# 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 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. diff --git a/docs/sources/permissions/overview.md b/docs/sources/permissions/overview.md new file mode 100644 index 00000000000..cd3fc5417b6 --- /dev/null +++ b/docs/sources/permissions/overview.md @@ -0,0 +1,42 @@ ++++ +title = "Overview" +description = "Overview for permissions" +keywords = ["grafana", "configuration", "documentation", "admin", "users", "datasources", "permissions"] +type = "docs" +aliases = ["/reference/admin", "/administration/permissions/"] +[menu.docs] +name = "Overview" +identifier = "overview-permissions" +parent = "permissions" +weight = 1 ++++ + +# Permissions Overview + +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, dashboards, datasources) +- The Grafana Admin (i.e. Super Admin) user flag. + +## 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. + +## 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. Learn more about [Organization Roles]({{< relref "permissions/organization_roles.md" >}}). + + +## Dashboard & Folder Permissions + +Dashboard and folder permissions allows you to remove the default role based permissions for Editors and Viewers and assign permissions to specific **Users** and **Teams**. Learn more about [Dashboard & Folder Permissions]({{< relref "permissions/dashboard_folder_permissions.md" >}}). + +## Datasource Permissions + +Per default, a datasource in an organization can be queried by any user in that organization. For example a user with `Viewer` role can still +issue any possible query to a data source, not just those queries that exist on dashboards he/she has access to. + +Datasource permissions allows you to change the default permissions for datasources and restrict query permissions to specific **Users** and **Teams**. Read more about [Datasource Permissions]({{< relref "permissions/datasource_permissions.md" >}}). diff --git a/docs/sources/reference/scripting.md b/docs/sources/reference/scripting.md index 7f218765d39..12ab91f3c3c 100644 --- a/docs/sources/reference/scripting.md +++ b/docs/sources/reference/scripting.md @@ -12,7 +12,7 @@ weight = 9 If you have lots of metric names that change (new servers etc) in a defined pattern it is irritating to constantly have to create new dashboards. -With scripted dashboards you can dynamically create your dashboards using javascript. In the folder grafana install folder +With scripted dashboards you can dynamically create your dashboards using javascript. In the grafana install folder under `public/dashboards/` there is a file named `scripted.js`. This file contains an example of a scripted dashboard. You can access it by using the url: `http://grafana_url/dashboard/script/scripted.js?rows=3&name=myName` diff --git a/docs/sources/whatsnew/index.md b/docs/sources/whatsnew/index.md index df472f07093..f4159643d72 100644 --- a/docs/sources/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 = 3 +weight = 5 +++ diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index a936d696207..c68cee50948 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -134,12 +134,16 @@ func AlertTest(c *m.ReqContext, dto dtos.AlertTestCommand) Response { OrgId: c.OrgId, Dashboard: dto.Dashboard, PanelId: dto.PanelId, + User: c.SignedInUser, } if err := bus.Dispatch(&backendCmd); err != nil { if validationErr, ok := err.(alerting.ValidationError); ok { return Error(422, validationErr.Error(), nil) } + if err == m.ErrDataSourceAccessDenied { + return Error(403, "Access denied to datasource", err) + } return Error(500, "Failed to test rule", err) } diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index 3bb2f236129..5cde0efd0b4 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -1,62 +1,22 @@ package api import ( - "fmt" - "github.com/pkg/errors" - "time" - "github.com/grafana/grafana/pkg/api/pluginproxy" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/metrics" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" ) -const HeaderNameNoBackendCache = "X-Grafana-NoCache" - -func (hs *HTTPServer) getDatasourceFromCache(id int64, c *m.ReqContext) (*m.DataSource, error) { - userPermissionsQuery := m.GetDataSourcePermissionsForUserQuery{ - User: c.SignedInUser, - } - if err := bus.Dispatch(&userPermissionsQuery); err != nil { - if err != bus.ErrHandlerNotFound { - return nil, err - } - } else { - permissionType, exists := userPermissionsQuery.Result[id] - if exists && permissionType != m.DsPermissionQuery { - return nil, errors.New("User not allowed to access datasource") - } - } - - nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true" - cacheKey := fmt.Sprintf("ds-%d", id) - - if !nocache { - if cached, found := hs.cache.Get(cacheKey); found { - ds := cached.(*m.DataSource) - if ds.OrgId == c.OrgId { - return ds, nil - } - } - } - - query := m.GetDataSourceByIdQuery{Id: id, OrgId: c.OrgId} - if err := bus.Dispatch(&query); err != nil { - return nil, err - } - - hs.cache.Set(cacheKey, query.Result, time.Second*5) - return query.Result, nil -} - func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) { c.TimeRequest(metrics.M_DataSource_ProxyReq_Timer) dsId := c.ParamsInt64(":id") - ds, err := hs.getDatasourceFromCache(dsId, c) - + ds, err := hs.DatasourceCache.GetDatasource(dsId, c.SignedInUser, c.SkipCache) if err != nil { + if err == m.ErrDataSourceAccessDenied { + c.JsonApiErr(403, "Access denied to datasource", err) + return + } c.JsonApiErr(500, "Unable to load datasource meta data", err) return } diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 858b3c5a8c5..ce28e4716ee 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -16,7 +16,6 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" - gocache "github.com/patrickmn/go-cache" macaron "gopkg.in/macaron.v1" "github.com/grafana/grafana/pkg/api/live" @@ -28,6 +27,8 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/cache" + "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/hooks" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/setting" @@ -46,19 +47,19 @@ type HTTPServer struct { macaron *macaron.Macaron context context.Context streamManager *live.StreamManager - cache *gocache.Cache httpSrv *http.Server - RouteRegister routing.RouteRegister `inject:""` - Bus bus.Bus `inject:""` - RenderService rendering.Service `inject:""` - Cfg *setting.Cfg `inject:""` - HooksService *hooks.HooksService `inject:""` + RouteRegister routing.RouteRegister `inject:""` + Bus bus.Bus `inject:""` + RenderService rendering.Service `inject:""` + Cfg *setting.Cfg `inject:""` + HooksService *hooks.HooksService `inject:""` + CacheService *cache.CacheService `inject:""` + DatasourceCache datasources.CacheService `inject:""` } func (hs *HTTPServer) Init() error { hs.log = log.New("http.server") - hs.cache = gocache.New(5*time.Minute, 10*time.Minute) hs.streamManager = live.NewStreamManager() hs.macaron = hs.newMacaron() @@ -231,6 +232,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() { m.Use(middleware.ValidateHostHeader(setting.Domain)) } + m.Use(middleware.HandleNoCacheHeader()) m.Use(middleware.AddDefaultResponseHeaders()) } diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index cb80bd346b8..6e5ae0f8761 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -25,8 +25,11 @@ func (hs *HTTPServer) QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) R return Error(400, "Query missing datasourceId", nil) } - ds, err := hs.getDatasourceFromCache(datasourceId, c) + ds, err := hs.DatasourceCache.GetDatasource(datasourceId, c.SignedInUser, c.SkipCache) if err != nil { + if err == m.ErrDataSourceAccessDenied { + return Error(403, "Access denied to datasource", err) + } return Error(500, "Unable to load datasource meta data", err) } diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index 765b8ddf993..2c67a06a843 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -15,13 +15,21 @@ import ( "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/bus" - _ "github.com/grafana/grafana/pkg/extensions" - "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/login" - _ "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" - _ "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/social" + + "golang.org/x/sync/errgroup" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/services/cache" + "github.com/grafana/grafana/pkg/setting" + + // self registering services + _ "github.com/grafana/grafana/pkg/extensions" + _ "github.com/grafana/grafana/pkg/metrics" + _ "github.com/grafana/grafana/pkg/plugins" _ "github.com/grafana/grafana/pkg/services/alerting" _ "github.com/grafana/grafana/pkg/services/cleanup" _ "github.com/grafana/grafana/pkg/services/notifications" @@ -29,10 +37,7 @@ import ( _ "github.com/grafana/grafana/pkg/services/rendering" _ "github.com/grafana/grafana/pkg/services/search" _ "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/social" // self registering services _ "github.com/grafana/grafana/pkg/tracing" - "golang.org/x/sync/errgroup" ) func NewGrafanaServer() *GrafanaServerImpl { @@ -72,6 +77,7 @@ func (g *GrafanaServerImpl) Run() error { serviceGraph.Provide(&inject.Object{Value: bus.GetBus()}) serviceGraph.Provide(&inject.Object{Value: g.cfg}) serviceGraph.Provide(&inject.Object{Value: routing.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)}) + serviceGraph.Provide(&inject.Object{Value: cache.New(5*time.Minute, 10*time.Minute)}) // self registered services services := registry.GetServices() @@ -138,7 +144,6 @@ func (g *GrafanaServerImpl) Run() error { } sendSystemdNotification("READY=1") - return g.childRoutines.Wait() } diff --git a/pkg/login/auth.go b/pkg/login/auth.go index 991fa72fd54..b766d963328 100644 --- a/pkg/login/auth.go +++ b/pkg/login/auth.go @@ -2,6 +2,7 @@ package login import ( "errors" + "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" ) diff --git a/pkg/middleware/headers.go b/pkg/middleware/headers.go new file mode 100644 index 00000000000..28c623d74b0 --- /dev/null +++ b/pkg/middleware/headers.go @@ -0,0 +1,14 @@ +package middleware + +import ( + m "github.com/grafana/grafana/pkg/models" + macaron "gopkg.in/macaron.v1" +) + +const HeaderNameNoBackendCache = "X-Grafana-NoCache" + +func HandleNoCacheHeader() macaron.Handler { + return func(ctx *m.ReqContext) { + ctx.SkipCache = ctx.Req.Header.Get(HeaderNameNoBackendCache) == "true" + } +} diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 7b29901c1a3..ace72d998eb 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -29,6 +29,7 @@ func GetContextHandler() macaron.Handler { Session: session.GetSession(), IsSignedIn: false, AllowAnonymous: false, + SkipCache: false, Logger: log.New("context"), } diff --git a/pkg/models/alert.go b/pkg/models/alert.go index ba1fc0779ba..aaf9c50197a 100644 --- a/pkg/models/alert.go +++ b/pkg/models/alert.go @@ -215,13 +215,14 @@ type AlertStateInfoDTO struct { // "Internal" commands type UpdateDashboardAlertsCommand struct { - UserId int64 OrgId int64 Dashboard *Dashboard + User *SignedInUser } type ValidateDashboardAlertsCommand struct { UserId int64 OrgId int64 Dashboard *Dashboard + User *SignedInUser } diff --git a/pkg/models/context.go b/pkg/models/context.go index c78028665a6..7cb80a957c3 100644 --- a/pkg/models/context.go +++ b/pkg/models/context.go @@ -20,6 +20,7 @@ type ReqContext struct { IsSignedIn bool IsRenderCall bool AllowAnonymous bool + SkipCache bool Logger log.Logger } diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index b71d17ec0d1..89439420d7a 100644 --- a/pkg/models/datasource.go +++ b/pkg/models/datasource.go @@ -207,11 +207,6 @@ func (p DsPermissionType) String() string { return names[int(p)] } -type GetDataSourcePermissionsForUserQuery struct { - User *SignedInUser - Result map[int64]DsPermissionType -} - type DatasourcesPermissionFilterQuery struct { User *SignedInUser Datasources []*DataSource diff --git a/pkg/models/user.go b/pkg/models/user.go index d5b912e0a9c..e3c7b556d35 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -165,6 +165,7 @@ type SignedInUser struct { IsAnonymous bool HelpFlags1 HelpFlags1 LastSeenAt time.Time + Teams []int64 } func (u *SignedInUser) ShouldUpdateLastSeenAt() bool { diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go index 87fca27f6c1..487a6db7927 100644 --- a/pkg/registry/registry.go +++ b/pkg/registry/registry.go @@ -29,11 +29,42 @@ func Register(descriptor *Descriptor) { } func GetServices() []*Descriptor { - sort.Slice(services, func(i, j int) bool { - return services[i].InitPriority > services[j].InitPriority + slice := getServicesWithOverrides() + + sort.Slice(slice, func(i, j int) bool { + return slice[i].InitPriority > slice[j].InitPriority }) - return services + return slice +} + +type OverrideServiceFunc func(descriptor Descriptor) (*Descriptor, bool) + +var overrides []OverrideServiceFunc + +func RegisterOverride(fn OverrideServiceFunc) { + overrides = append(overrides, fn) +} + +func getServicesWithOverrides() []*Descriptor { + slice := []*Descriptor{} + for _, s := range services { + var descriptor *Descriptor + for _, fn := range overrides { + if newDescriptor, override := fn(*s); override { + descriptor = newDescriptor + break + } + } + + if descriptor != nil { + slice = append(slice, descriptor) + } else { + slice = append(slice, s) + } + } + + return slice } // Service interface is the lowest common shape that services diff --git a/pkg/services/alerting/commands.go b/pkg/services/alerting/commands.go index 02186d697ee..dd2ff5658d6 100644 --- a/pkg/services/alerting/commands.go +++ b/pkg/services/alerting/commands.go @@ -11,7 +11,7 @@ func init() { } func validateDashboardAlerts(cmd *m.ValidateDashboardAlertsCommand) error { - extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId) + extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId, cmd.User) return extractor.ValidateAlerts() } @@ -19,11 +19,11 @@ func validateDashboardAlerts(cmd *m.ValidateDashboardAlertsCommand) error { func updateDashboardAlerts(cmd *m.UpdateDashboardAlertsCommand) error { saveAlerts := m.SaveAlertsCommand{ OrgId: cmd.OrgId, - UserId: cmd.UserId, + UserId: cmd.User.UserId, DashboardId: cmd.Dashboard.Id, } - extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId) + extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId, cmd.User) alerts, err := extractor.GetAlerts() if err != nil { diff --git a/pkg/services/alerting/conditions/reducer_test.go b/pkg/services/alerting/conditions/reducer_test.go index 9d4e1462690..7f11fc498bd 100644 --- a/pkg/services/alerting/conditions/reducer_test.go +++ b/pkg/services/alerting/conditions/reducer_test.go @@ -52,6 +52,24 @@ func TestSimpleReducer(t *testing.T) { So(result, ShouldEqual, float64(1)) }) + Convey("median should ignore null values", func() { + reducer := NewSimpleReducer("median") + series := &tsdb.TimeSeries{ + Name: "test time serie", + } + + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 1)) + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2)) + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 3)) + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(float64(1)), 4)) + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(float64(2)), 5)) + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(float64(3)), 6)) + + result := reducer.Reduce(series) + So(result.Valid, ShouldEqual, true) + So(result.Float64, ShouldEqual, float64(2)) + }) + Convey("avg", func() { result := testReducer("avg", 1, 2, 3) So(result, ShouldEqual, float64(2)) diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index edfab2dedee..0abacc91313 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -13,14 +13,16 @@ import ( // DashAlertExtractor extracts alerts from the dashboard json type DashAlertExtractor struct { + User *m.SignedInUser Dash *m.Dashboard OrgID int64 log log.Logger } // NewDashAlertExtractor returns a new DashAlertExtractor -func NewDashAlertExtractor(dash *m.Dashboard, orgID int64) *DashAlertExtractor { +func NewDashAlertExtractor(dash *m.Dashboard, orgID int64, user *m.SignedInUser) *DashAlertExtractor { return &DashAlertExtractor{ + User: user, Dash: dash, OrgID: orgID, log: log.New("alerting.extractor"), @@ -149,6 +151,21 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, return nil, ValidationError{Reason: fmt.Sprintf("Data source used by alert rule not found, alertName=%v, datasource=%s", alert.Name, dsName)} } + dsFilterQuery := m.DatasourcesPermissionFilterQuery{ + User: e.User, + Datasources: []*m.DataSource{datasource}, + } + + if err := bus.Dispatch(&dsFilterQuery); err != nil { + if err != bus.ErrHandlerNotFound { + return nil, err + } + } else { + if len(dsFilterQuery.Result) == 0 { + return nil, m.ErrDataSourceAccessDenied + } + } + jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id) if interval, err := panel.Get("interval").String(); err == nil { diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go index e2dc01a1181..0890b9e1bd1 100644 --- a/pkg/services/alerting/extractor_test.go +++ b/pkg/services/alerting/extractor_test.go @@ -69,7 +69,7 @@ func TestAlertRuleExtraction(t *testing.T) { So(getTarget(dashJson), ShouldEqual, "") }) - extractor := NewDashAlertExtractor(dash, 1) + extractor := NewDashAlertExtractor(dash, 1, nil) _, _ = extractor.GetAlerts() Convey("Dashboard json should not be updated after extracting rules", func() { @@ -83,7 +83,7 @@ func TestAlertRuleExtraction(t *testing.T) { So(err, ShouldBeNil) dash := m.NewDashboardFromJson(dashJson) - extractor := NewDashAlertExtractor(dash, 1) + extractor := NewDashAlertExtractor(dash, 1, nil) alerts, err := extractor.GetAlerts() @@ -146,7 +146,7 @@ func TestAlertRuleExtraction(t *testing.T) { dashJson, err := simplejson.NewJson(panelWithoutId) So(err, ShouldBeNil) dash := m.NewDashboardFromJson(dashJson) - extractor := NewDashAlertExtractor(dash, 1) + extractor := NewDashAlertExtractor(dash, 1, nil) _, err = extractor.GetAlerts() @@ -162,7 +162,7 @@ func TestAlertRuleExtraction(t *testing.T) { dashJson, err := simplejson.NewJson(panelWithIdZero) So(err, ShouldBeNil) dash := m.NewDashboardFromJson(dashJson) - extractor := NewDashAlertExtractor(dash, 1) + extractor := NewDashAlertExtractor(dash, 1, nil) _, err = extractor.GetAlerts() @@ -178,7 +178,7 @@ func TestAlertRuleExtraction(t *testing.T) { dashJson, err := simplejson.NewJson(json) So(err, ShouldBeNil) dash := m.NewDashboardFromJson(dashJson) - extractor := NewDashAlertExtractor(dash, 1) + extractor := NewDashAlertExtractor(dash, 1, nil) alerts, err := extractor.GetAlerts() @@ -198,7 +198,7 @@ func TestAlertRuleExtraction(t *testing.T) { dashJson, err := simplejson.NewJson(json) So(err, ShouldBeNil) dash := m.NewDashboardFromJson(dashJson) - extractor := NewDashAlertExtractor(dash, 1) + extractor := NewDashAlertExtractor(dash, 1, nil) alerts, err := extractor.GetAlerts() @@ -228,7 +228,7 @@ func TestAlertRuleExtraction(t *testing.T) { So(err, ShouldBeNil) dash := m.NewDashboardFromJson(dashJson) - extractor := NewDashAlertExtractor(dash, 1) + extractor := NewDashAlertExtractor(dash, 1, nil) alerts, err := extractor.GetAlerts() @@ -248,7 +248,7 @@ func TestAlertRuleExtraction(t *testing.T) { dashJSON, err := simplejson.NewJson(json) So(err, ShouldBeNil) dash := m.NewDashboardFromJson(dashJSON) - extractor := NewDashAlertExtractor(dash, 1) + extractor := NewDashAlertExtractor(dash, 1, nil) err = extractor.ValidateAlerts() diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index 374b49ea957..ca5f47a322f 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -39,6 +39,39 @@ func init() { Override default channel or user, use #channel-name or @username +Visualization has no options
; } } + onPanelOptionsChanged = (options: any) => { + this.props.panel.updateOptions(options); + this.forceUpdate(); + }; + renderVizTab() { return (