Merge branch 'master' into backend_plugins

* master: (584 commits)
  prometheus: change default resolution to 1/1
  fix: viewers can edit now works correctly
  fix: fixed minor ux and firefox issues, fixes #10228
  ux: minor fixes
  profile: use name or fallback for profile page
  fix: sidemenu profile main text is now username instead of name
  build: update master version to 5.0.0-pre1
  dashfolder: change to migration text
  ux:s sidemenu icon rules
  teams: add team count when searching for team
  changed background color for infobox and new blues in light theme, light theme now uses blue-dark in panel query (#10211)
  ux: fixed navbar issue when sidemenu closes
  ux: minor position change for layout selector, fixes #10217
  fix: view json from share modal now works, #10217
  ux: used new add data sources icon
  dashfolders: styling of selected filters
  dashfolders: styling of selected filters
  dashfolders: fix moving plugin dashboard to folder
  changelog: adds note about closing #9170
  dashfolders: fix folder selection dropdown in dashboard settings
  ...
This commit is contained in:
bergquist
2017-12-15 14:34:55 +01:00
579 changed files with 32938 additions and 10392 deletions

3
.gitignore vendored
View File

@@ -51,6 +51,9 @@ debug.test
/packaging/**/*.rpm /packaging/**/*.rpm
/packaging/**/*.deb /packaging/**/*.deb
# Ignore OSX indexing
.DS_Store
/vendor/**/*.py /vendor/**/*.py
/vendor/**/*.xml /vendor/**/*.xml
/vendor/**/*.yml /vendor/**/*.yml

View File

@@ -1,13 +1,20 @@
# 5.0.0 (unreleased) # 5.0.0 (unreleased / master branch)
### WIP (in develop branch currently as its unstable or unfinished) ### New Features
- Dashboard folders - **Dashboards** Dashboard folders, [#1611](https://github.com/grafana/grafana/issues/1611)
- User groups - **Teams** User groups (teams) implemented. Can be used in folder & dashboard permission list.
- Dashboard permissions (on folder & dashboard level), permissions can be assigned to groups or individual users - **Dashboard grid**: Panels are now layed out in a two dimensional grid (with x, y, w, h). [#9093](https://github.com/grafana/grafana/issues/9093).
- UX changes to nav & side menu - **Templating**: Vertical repeat direction for panel repeats.
- New dashboard grid layout system - **UX**: Major update to page header and navigation
- **Dashboard settings**: Combine dashboard settings views into one with side menu, [#9750](https://github.com/grafana/grafana/issues/9750)
# 4.7.0 (unreleased) ## New Dashboard Grid
The new grid engine is major upgrade for how you can position and move panels. It enables new layouts and a much easier dashboard building experience. The change is backwards compatible. Grafana will automatically upgrade your dashboards to the new schema and position panels to match your existing layout. There might be minor differences in panel height.
Dashboard panels and rows are positioned using a gridPos object `{x: 0, y: 0, w: 24, h: 5}`. Units are in grid dimensions (24 columns, 1 height unit 30px). Rows and Panels objects exist (together) in a flat array directly on the dashboard root object. Rows are not needed for layouts anymore and are mainly there for backward compatibility. Some panel plugins that do not respect their panel height might require an update.
# 4.7.0 (unreleased / v4.7.x branch)
## Breaking changes ## Breaking changes
@@ -25,7 +32,8 @@ From `/etc/grafana/datasources` to `/etc/grafana/provisioning/datasources` when
* **Graphite**: Query editor updated to support new query by tag features [#9230](https://github.com/grafana/grafana/issues/9230) * **Graphite**: Query editor updated to support new query by tag features [#9230](https://github.com/grafana/grafana/issues/9230)
* **Dashboard history**: New config file option versions_to_keep sets how many versions per dashboard to store, [#9671](https://github.com/grafana/grafana/issues/9671) * **Dashboard history**: New config file option versions_to_keep sets how many versions per dashboard to store, [#9671](https://github.com/grafana/grafana/issues/9671)
* **Dashboard as cfg**: Load dashboards from file into Grafana on startup/change [#9654](https://github.com/grafana/grafana/issues/9654) [#5269](https://github.com/grafana/grafana/issues/5269) * **Dashboard as cfg**: Load dashboards from file into Grafana on startup/change [#9654](https://github.com/grafana/grafana/issues/9654) [#5269](https://github.com/grafana/grafana/issues/5269)
* **Prometheus**: Grafana can now send alerts to Prometheus Alertmanager while firing [#7481](https://github.com/grafana/grafana/issues/7481), thx [@Thib17](https://github.com/Thib17) and [@mtanda](https://github.com/mtanda)
* **Table**: Support multiple table formated queries in table panel [#9170](https://github.com/grafana/grafana/issues/9170), thx [@davkal](https://github.com/davkal)
## Minor ## Minor
* **Alert panel**: Adds placeholder text when no alerts are within the time range [#9624](https://github.com/grafana/grafana/issues/9624), thx [@straend](https://github.com/straend) * **Alert panel**: Adds placeholder text when no alerts are within the time range [#9624](https://github.com/grafana/grafana/issues/9624), thx [@straend](https://github.com/straend)
* **Mysql**: MySQL enable MaxOpenCon and MaxIdleCon regards how constring is configured. [#9784](https://github.com/grafana/grafana/issues/9784), thx [@dfredell](https://github.com/dfredell) * **Mysql**: MySQL enable MaxOpenCon and MaxIdleCon regards how constring is configured. [#9784](https://github.com/grafana/grafana/issues/9784), thx [@dfredell](https://github.com/dfredell)
@@ -46,11 +54,13 @@ From `/etc/grafana/datasources` to `/etc/grafana/provisioning/datasources` when
* **Singlestat**: suppress error when result contains no datapoints [#9636](https://github.com/grafana/grafana/issues/9636), thx [@utkarshcmu](https://github.com/utkarshcmu) * **Singlestat**: suppress error when result contains no datapoints [#9636](https://github.com/grafana/grafana/issues/9636), thx [@utkarshcmu](https://github.com/utkarshcmu)
* **Postgres/MySQL**: Control quoting in SQL-queries when using template variables [#9030](https://github.com/grafana/grafana/issues/9030), thanks [@svenklemm](https://github.com/svenklemm) * **Postgres/MySQL**: Control quoting in SQL-queries when using template variables [#9030](https://github.com/grafana/grafana/issues/9030), thanks [@svenklemm](https://github.com/svenklemm)
# 4.6.3 (unreleased) # 4.6.3 (2017-12-14)
## Fixes ## Fixes
* **Gzip**: Fixes bug gravatar images when gzip was enabled [#5952](https://github.com/grafana/grafana/issues/5952) * **Gzip**: Fixes bug gravatar images when gzip was enabled [#5952](https://github.com/grafana/grafana/issues/5952)
* **Alert list**: Now shows alert state changes even after adding manual annotations on dashboard [#9951](https://github.com/grafana/grafana/issues/9951) * **Alert list**: Now shows alert state changes even after adding manual annotations on dashboard [#9951](https://github.com/grafana/grafana/issues/9951)
* **Alerting**: Fixes bug where rules evaluated as firing when all conditions was false and using OR operator. [#9318](https://github.com/grafana/grafana/issues/9318)
* **Cloudwatch**: CloudWatch no longer display metrics' default alias [#10151](https://github.com/grafana/grafana/issues/10151), thx [@mtanda](https://github.com/mtanda)
# 4.6.2 (2017-11-16) # 4.6.2 (2017-11-16)

View File

@@ -19,7 +19,7 @@ If you have any problems please read the [troubleshooting guide](http://docs.gra
Be sure to read the [getting started guide](http://docs.grafana.org/guides/gettingstarted/) and the other feature guides. Be sure to read the [getting started guide](http://docs.grafana.org/guides/gettingstarted/) and the other feature guides.
## Run from master ## Run from master
If you want to build a package yourself, or contribute. Here is a guide for how to do that. You can always find If you want to build a package yourself, or contribute - Here is a guide for how to do that. You can always find
the latest master builds [here](https://grafana.com/grafana/download) the latest master builds [here](https://grafana.com/grafana/download)
### Dependencies ### Dependencies
@@ -97,7 +97,7 @@ Writing & watching frontend tests (we have two test runners)
## Contribute ## Contribute
If you have any idea for an improvement or found a bug do not hesitate to open an issue. If you have any idea for an improvement or found a bug, do not hesitate to open an issue.
And if you have time clone this repo and submit a pull request and help me make Grafana And if you have time clone this repo and submit a pull request and help me make Grafana
the kickass metrics & devops dashboard we all dream about! the kickass metrics & devops dashboard we all dream about!

View File

@@ -6,7 +6,7 @@ But it will give you an idea of our current vision and plan.
### Short term (1-4 months) ### Short term (1-4 months)
- Release Grafana v5 - Release Grafana v5
- User groups - Teams
- Dashboard folders - Dashboard folders
- Dashboard & folder permissions (assigned to users or groups) - Dashboard & folder permissions (assigned to users or groups)
- New Dashboard layout engine - New Dashboard layout engine

View File

@@ -221,6 +221,9 @@ external_manage_link_url =
external_manage_link_name = external_manage_link_name =
external_manage_info = external_manage_info =
# Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard.
viewers_can_edit = false
[auth] [auth]
# Set to true to disable (hide) the login form, useful if you use OAuth # Set to true to disable (hide) the login form, useful if you use OAuth
disable_login_form = false disable_login_form = false

View File

@@ -205,6 +205,9 @@ log_queries =
;external_manage_link_name = ;external_manage_link_name =
;external_manage_info = ;external_manage_info =
# Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard.
;viewers_can_edit = false
[auth] [auth]
# Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false # Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false
;disable_login_form = false ;disable_login_form = false

View File

@@ -1,4 +1,4 @@
graphite: graphite09:
build: blocks/graphite build: blocks/graphite
ports: ports:
- "8080:80" - "8080:80"

View File

@@ -126,25 +126,26 @@ There are couple of configurations options which need to be set in Grafana UI un
Once these two properties are set, you can send the alerts to Kafka for further processing or throttling them. Once these two properties are set, you can send the alerts to Kafka for further processing or throttling them.
### Other Supported Notification Channels ### All supported notifier
Grafana also supports the following Notification Channels: Name | Type |Support images
-----|------------ | ------
Slack | `slack` | yes
Pagerduty | `pagerduty` | yes
Email | `email` | yes
Webhook | `webhook` | link
Kafka | `kafka` | no
Hipchat | `hipchat` | yes
VictorOps | `victorops` | yes
Sensu | `sensu` | yes
OpsGenie | `opsgenie` | yes
Threema | `threema` | yes
Pushover | `pushover` | no
Telegram | `telegram` | no
Line | `line` | no
Prometheus Alertmanager | `prometheus-alertmanager` | no
- HipChat
- VictorOps
- Sensu
- OpsGenie
- Threema
- Pushover
- Telegram
- LINE
# Enable images in notifications {#external-image-store} # Enable images in notifications {#external-image-store}

View File

@@ -196,6 +196,8 @@ Content-Type: application/json
**Example Request**: **Example Request**:
```http ```http
DELETE /api/alert-notifications/1 HTTP/1.1
Accept: application/json
Content-Type: application/json Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
``` ```

View File

@@ -100,7 +100,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
**Example Request**: **Example Request**:
```http ```http
DELETE /api/auth/keys/3 HTTP/1.1 DELETE /api/auth/keys/3 HTTP/1.1
Accept: application/json Accept: application/json
Content-Type: application/json Content-Type: application/json

View File

@@ -205,7 +205,7 @@ The database user (not applicable for `sqlite3`).
### password ### password
The database user's password (not applicable for `sqlite3`). If the password contains `#` or `;` you have to wrap it with trippel quotes. Ex `"""#password;"""` The database user's password (not applicable for `sqlite3`). If the password contains `#` or `;` you have to wrap it with triple quotes. Ex `"""#password;"""`
### ssl_mode ### ssl_mode
@@ -214,19 +214,19 @@ For MySQL, use either `true`, `false`, or `skip-verify`.
### ca_cert_path ### ca_cert_path
(MySQL only) The path to the CA certificate to use. On many linux systems, certs can be found in `/etc/ssl/certs`. The path to the CA certificate to use. On many linux systems, certs can be found in `/etc/ssl/certs`.
### client_key_path ### client_key_path
(MySQL only) The path to the client key. Only if server requires client authentication. The path to the client key. Only if server requires client authentication.
### client_cert_path ### client_cert_path
(MySQL only) The path to the client cert. Only if server requires client authentication. The path to the client cert. Only if server requires client authentication.
### server_cert_name ### server_cert_name
(MySQL only) The common name field of the certificate used by the `mysql` server. Not necessary if `ssl_mode` is set to `skip-verify`. The common name field of the certificate used by the `mysql` or `postgres` server. Not necessary if `ssl_mode` is set to `skip-verify`.
### max_idle_conn ### max_idle_conn
The maximum number of connections in the idle connection pool. The maximum number of connections in the idle connection pool.
@@ -292,10 +292,14 @@ organization to be created for that new user.
The role new users will be assigned for the main organization (if the The role new users will be assigned for the main organization (if the
above setting is set to true). Defaults to `Viewer`, other valid above setting is set to true). Defaults to `Viewer`, other valid
options are `Admin` and `Editor` and `Read Only Editor`. e.g. : options are `Admin` and `Editor`. e.g. :
`auto_assign_org_role = Read Only Editor` `auto_assign_org_role = Viewer`
### viewers can edit
Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard.
Defaults to `false`.
<hr> <hr>

View File

@@ -15,9 +15,7 @@ weight = 1
Description | Download Description | Download
------------ | ------------- ------------ | -------------
Stable for Debian-based Linux | [grafana_4.6.2_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.6.2_amd64.deb) Stable for Debian-based Linux | [grafana_4.6.3_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.6.3_amd64.deb)
<!-- Beta for Debian-based Linux | [grafana_4.5.0-beta1_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.5.0-beta1_amd64.deb) -->
Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
installation. installation.
@@ -26,21 +24,10 @@ installation.
```bash ```bash
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.6.2_amd64.deb wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.6.3_amd64.deb
sudo apt-get install -y adduser libfontconfig sudo apt-get install -y adduser libfontconfig
sudo dpkg -i grafana_4.6.2_amd64.deb 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_4.5.2-beta1_amd64.deb
sudo apt-get install -y adduser libfontconfig
sudo dpkg -i grafana_4.5.2-beta1_amd64.deb
```
-->
## APT Repository ## APT Repository
Add the following line to your `/etc/apt/sources.list` file. Add the following line to your `/etc/apt/sources.list` file.

View File

@@ -15,9 +15,7 @@ weight = 2
Description | Download Description | Download
------------ | ------------- ------------ | -------------
Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [4.6.2 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.2-1.x86_64.rpm) Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [4.6.3 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.3-1.x86_64.rpm)
<!-- Latest Beta for CentOS / Fedora / OpenSuse / Redhat Linux | [4.5.0-beta1 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.5.0-beta1.x86_64.rpm) -->
Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
installation. installation.
@@ -27,7 +25,7 @@ installation.
You can install Grafana using Yum directly. You can install Grafana using Yum directly.
```bash ```bash
$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.2-1.x86_64.rpm $ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.3-1.x86_64.rpm
``` ```
Or install manually using `rpm`. Or install manually using `rpm`.
@@ -35,15 +33,15 @@ Or install manually using `rpm`.
#### On CentOS / Fedora / Redhat: #### On CentOS / Fedora / Redhat:
```bash ```bash
$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.2-1.x86_64.rpm $ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.3-1.x86_64.rpm
$ sudo yum install initscripts fontconfig $ sudo yum install initscripts fontconfig
$ sudo rpm -Uvh grafana-4.6.2-1.x86_64.rpm $ sudo rpm -Uvh grafana-4.6.3-1.x86_64.rpm
``` ```
#### On OpenSuse: #### On OpenSuse:
```bash ```bash
$ sudo rpm -i --nodeps grafana-4.6.2-1.x86_64.rpm $ sudo rpm -i --nodeps grafana-4.6.3-1.x86_64.rpm
``` ```
## Install via YUM Repository ## Install via YUM Repository

View File

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

View File

@@ -1,6 +1,6 @@
module.exports = { module.exports = {
verbose: true, verbose: false,
"globals": { "globals": {
"ts-jest": { "ts-jest": {
"tsConfigFile": "tsconfig.json" "tsConfigFile": "tsconfig.json"

View File

@@ -4,7 +4,7 @@
"company": "Grafana Labs" "company": "Grafana Labs"
}, },
"name": "grafana", "name": "grafana",
"version": "4.7.0-pre1", "version": "5.0.0-pre1",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "http://github.com/grafana/grafana.git" "url": "http://github.com/grafana/grafana.git"
@@ -14,8 +14,8 @@
"@types/enzyme": "^2.8.9", "@types/enzyme": "^2.8.9",
"@types/jest": "^21.1.4", "@types/jest": "^21.1.4",
"@types/node": "^8.0.31", "@types/node": "^8.0.31",
"@types/react": "^16.0.5", "@types/react": "^16.0.25",
"@types/react-dom": "^15.5.4", "@types/react-dom": "^16.0.3",
"angular-mocks": "^1.6.6", "angular-mocks": "^1.6.6",
"autoprefixer": "^6.4.0", "autoprefixer": "^6.4.0",
"awesome-typescript-loader": "^3.2.3", "awesome-typescript-loader": "^3.2.3",
@@ -115,22 +115,26 @@
"angular-sanitize": "^1.6.6", "angular-sanitize": "^1.6.6",
"babel-polyfill": "^6.26.0", "babel-polyfill": "^6.26.0",
"brace": "^0.10.0", "brace": "^0.10.0",
"classnames": "^2.2.5",
"clipboard": "^1.7.1", "clipboard": "^1.7.1",
"eventemitter3": "^2.0.3", "d3": "^4.11.0",
"d3-scale-chromatic": "^1.1.1",
"eventemitter3": "^2.0.2",
"file-saver": "^1.3.3", "file-saver": "^1.3.3",
"jquery": "^3.2.1", "jquery": "^3.2.1",
"lodash": "^4.17.4", "lodash": "^4.17.4",
"moment": "^2.18.1", "moment": "^2.18.1",
"mousetrap": "^1.6.0", "mousetrap": "^1.6.0",
"ngreact": "^0.4.1", "perfect-scrollbar": "^1.2.0",
"react": "^16.0.0", "prop-types": "^15.6.0",
"react-dom": "^16.0.0", "react": "^16.1.1",
"react-dom": "^16.1.1",
"react-grid-layout": "^0.16.1",
"react-sizeme": "^2.3.6",
"remarkable": "^1.7.1", "remarkable": "^1.7.1",
"rxjs": "^5.4.3", "rxjs": "^5.4.3",
"tether": "^1.4.0", "tether": "^1.4.0",
"tether-drop": "https://github.com/torkelo/drop", "tether-drop": "https://github.com/torkelo/drop",
"tinycolor2": "^1.4.1", "tinycolor2": "^1.4.1"
"d3": "^4.11.0",
"d3-scale-chromatic": "^1.1.1"
} }
} }

View File

@@ -1,5 +1,5 @@
#! /usr/bin/env bash #! /usr/bin/env bash
version=4.6.2 version=4.6.3
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_${version}_amd64.deb wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_${version}_amd64.deb

View File

@@ -40,9 +40,11 @@ func (hs *HttpServer) registerRoutes() {
r.Get("/datasources/", reqSignedIn, Index) r.Get("/datasources/", reqSignedIn, Index)
r.Get("/datasources/new", reqSignedIn, Index) r.Get("/datasources/new", reqSignedIn, Index)
r.Get("/datasources/edit/*", reqSignedIn, Index) r.Get("/datasources/edit/*", reqSignedIn, Index)
r.Get("/org/users/", reqSignedIn, Index) r.Get("/org/users/new", reqSignedIn, Index)
r.Get("/org/users/invite", reqSignedIn, Index)
r.Get("/org/apikeys/", reqSignedIn, Index) r.Get("/org/apikeys/", reqSignedIn, Index)
r.Get("/dashboard/import/", reqSignedIn, Index) r.Get("/dashboard/import/", reqSignedIn, Index)
r.Get("/configuration", reqGrafanaAdmin, Index)
r.Get("/admin", reqGrafanaAdmin, Index) r.Get("/admin", reqGrafanaAdmin, Index)
r.Get("/admin/settings", reqGrafanaAdmin, Index) r.Get("/admin/settings", reqGrafanaAdmin, Index)
r.Get("/admin/users", reqGrafanaAdmin, Index) r.Get("/admin/users", reqGrafanaAdmin, Index)
@@ -62,6 +64,7 @@ func (hs *HttpServer) registerRoutes() {
r.Get("/dashboard-solo/snapshot/*", Index) r.Get("/dashboard-solo/snapshot/*", Index)
r.Get("/dashboard-solo/*", reqSignedIn, Index) r.Get("/dashboard-solo/*", reqSignedIn, Index)
r.Get("/import/dashboard", reqSignedIn, Index) r.Get("/import/dashboard", reqSignedIn, Index)
r.Get("/dashboards/", reqSignedIn, Index)
r.Get("/dashboards/*", reqSignedIn, Index) r.Get("/dashboards/*", reqSignedIn, Index)
r.Get("/playlists/", reqSignedIn, Index) r.Get("/playlists/", reqSignedIn, Index)
@@ -134,6 +137,18 @@ func (hs *HttpServer) registerRoutes() {
usersRoute.Post("/:id/using/:orgId", wrap(UpdateUserActiveOrg)) usersRoute.Post("/:id/using/:orgId", wrap(UpdateUserActiveOrg))
}, reqGrafanaAdmin) }, reqGrafanaAdmin)
// team (admin permission required)
apiRoute.Group("/teams", func(teamsRoute RouteRegister) {
teamsRoute.Get("/:teamId", wrap(GetTeamById))
teamsRoute.Get("/search", wrap(SearchTeams))
teamsRoute.Post("/", quota("teams"), bind(m.CreateTeamCommand{}), wrap(CreateTeam))
teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), wrap(UpdateTeam))
teamsRoute.Delete("/:teamId", wrap(DeleteTeamById))
teamsRoute.Get("/:teamId/members", wrap(GetTeamMembers))
teamsRoute.Post("/:teamId/members", quota("teams"), bind(m.AddTeamMemberCommand{}), wrap(AddTeamMember))
teamsRoute.Delete("/:teamId/members/:userId", wrap(RemoveTeamMember))
}, reqOrgAdmin)
// org information available to all users. // org information available to all users.
apiRoute.Group("/org", func(orgRoute RouteRegister) { apiRoute.Group("/org", func(orgRoute RouteRegister) {
orgRoute.Get("/", wrap(GetOrgCurrent)) orgRoute.Get("/", wrap(GetOrgCurrent))
@@ -224,12 +239,8 @@ func (hs *HttpServer) registerRoutes() {
// Dashboard // Dashboard
apiRoute.Group("/dashboards", func(dashboardRoute RouteRegister) { apiRoute.Group("/dashboards", func(dashboardRoute RouteRegister) {
dashboardRoute.Get("/db/:slug", GetDashboard) dashboardRoute.Get("/db/:slug", wrap(GetDashboard))
dashboardRoute.Delete("/db/:slug", reqEditorRole, DeleteDashboard) dashboardRoute.Delete("/db/:slug", reqEditorRole, wrap(DeleteDashboard))
dashboardRoute.Get("/id/:dashboardId/versions", wrap(GetDashboardVersions))
dashboardRoute.Get("/id/:dashboardId/versions/:id", wrap(GetDashboardVersion))
dashboardRoute.Post("/id/:dashboardId/restore", reqEditorRole, bind(dtos.RestoreDashboardVersionCommand{}), wrap(RestoreDashboardVersion))
dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), wrap(CalculateDashboardDiff)) dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), wrap(CalculateDashboardDiff))
@@ -237,6 +248,18 @@ func (hs *HttpServer) registerRoutes() {
dashboardRoute.Get("/home", wrap(GetHomeDashboard)) dashboardRoute.Get("/home", wrap(GetHomeDashboard))
dashboardRoute.Get("/tags", GetDashboardTags) dashboardRoute.Get("/tags", GetDashboardTags)
dashboardRoute.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard)) dashboardRoute.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard))
dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute RouteRegister) {
dashIdRoute.Get("/versions", wrap(GetDashboardVersions))
dashIdRoute.Get("/versions/:id", wrap(GetDashboardVersion))
dashIdRoute.Post("/restore", reqEditorRole, bind(dtos.RestoreDashboardVersionCommand{}), wrap(RestoreDashboardVersion))
dashIdRoute.Group("/acl", func(aclRoute RouteRegister) {
aclRoute.Get("/", wrap(GetDashboardAclList))
aclRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateDashboardAcl))
aclRoute.Delete("/:aclId", wrap(DeleteDashboardAcl))
})
})
}) })
// Dashboard snapshots // Dashboard snapshots

View File

@@ -25,6 +25,8 @@ import (
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"gopkg.in/macaron.v1" "gopkg.in/macaron.v1"
gocache "github.com/patrickmn/go-cache"
) )
var gravatarSource string var gravatarSource string
@@ -92,7 +94,7 @@ func (this *Avatar) Update() (err error) {
type CacheServer struct { type CacheServer struct {
notFound *Avatar notFound *Avatar
cache map[string]*Avatar cache *gocache.Cache
} }
func (this *CacheServer) mustInt(r *http.Request, defaultValue int, keys ...string) (v int) { func (this *CacheServer) mustInt(r *http.Request, defaultValue int, keys ...string) (v int) {
@@ -110,7 +112,9 @@ func (this *CacheServer) Handler(ctx *macaron.Context) {
var avatar *Avatar var avatar *Avatar
if avatar, _ = this.cache[hash]; avatar == nil { if obj, exist := this.cache.Get(hash); exist {
avatar = obj.(*Avatar)
} else {
avatar = New(hash) avatar = New(hash)
} }
@@ -124,7 +128,7 @@ func (this *CacheServer) Handler(ctx *macaron.Context) {
if avatar.notFound { if avatar.notFound {
avatar = this.notFound avatar = this.notFound
} else { } else {
this.cache[hash] = avatar this.cache.Add(hash, avatar, gocache.DefaultExpiration)
} }
ctx.Resp.Header().Add("Content-Type", "image/jpeg") ctx.Resp.Header().Add("Content-Type", "image/jpeg")
@@ -146,7 +150,7 @@ func NewCacheServer() *CacheServer {
return &CacheServer{ return &CacheServer{
notFound: newNotFound(), notFound: newNotFound(),
cache: make(map[string]*Avatar), cache: gocache.New(time.Hour, time.Hour*2),
} }
} }

View File

@@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"os" "os"
"path" "path"
"strings"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
@@ -18,6 +17,7 @@ import (
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
@@ -35,23 +35,34 @@ func isDashboardStarredByUser(c *middleware.Context, dashId int64) (bool, error)
return query.Result, nil return query.Result, nil
} }
func GetDashboard(c *middleware.Context) { func dashboardGuardianResponse(err error) Response {
slug := strings.ToLower(c.Params(":slug"))
query := m.GetDashboardQuery{Slug: slug, OrgId: c.OrgId}
err := bus.Dispatch(&query)
if err != nil { if err != nil {
c.JsonApiErr(404, "Dashboard not found", nil) return ApiError(500, "Error while checking dashboard permissions", err)
return } else {
return ApiError(403, "Access denied to this dashboard", nil)
}
} }
isStarred, err := isDashboardStarredByUser(c, query.Result.Id) func GetDashboard(c *middleware.Context) Response {
if err != nil { dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0)
c.JsonApiErr(500, "Error while checking if dashboard was starred by user", err) if rsp != nil {
return return rsp
} }
dash := query.Result guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
if canView, err := guardian.CanView(); err != nil || !canView {
fmt.Printf("%v", err)
return dashboardGuardianResponse(err)
}
canEdit, _ := guardian.CanEdit()
canSave, _ := guardian.CanSave()
canAdmin, _ := guardian.CanAdmin()
isStarred, err := isDashboardStarredByUser(c, dash.Id)
if err != nil {
return ApiError(500, "Error while checking if dashboard was starred by user", err)
}
// Finding creator and last updater of the dashboard // Finding creator and last updater of the dashboard
updater, creator := "Anonymous", "Anonymous" updater, creator := "Anonymous", "Anonymous"
@@ -62,29 +73,44 @@ func GetDashboard(c *middleware.Context) {
creator = getUserLogin(dash.CreatedBy) creator = getUserLogin(dash.CreatedBy)
} }
// make sure db version is in sync with json model version meta := dtos.DashboardMeta{
dash.Data.Set("version", dash.Version)
dto := dtos.DashboardFullWithMeta{
Dashboard: dash.Data,
Meta: dtos.DashboardMeta{
IsStarred: isStarred, IsStarred: isStarred,
Slug: slug, Slug: dash.Slug,
Type: m.DashTypeDB, Type: m.DashTypeDB,
CanStar: c.IsSignedIn, CanStar: c.IsSignedIn,
CanSave: c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR, CanSave: canSave,
CanEdit: canEditDashboard(c.OrgRole), CanEdit: canEdit,
CanAdmin: canAdmin,
Created: dash.Created, Created: dash.Created,
Updated: dash.Updated, Updated: dash.Updated,
UpdatedBy: updater, UpdatedBy: updater,
CreatedBy: creator, CreatedBy: creator,
Version: dash.Version, Version: dash.Version,
}, HasAcl: dash.HasAcl,
IsFolder: dash.IsFolder,
FolderId: dash.FolderId,
FolderTitle: "Root",
}
// lookup folder title
if dash.FolderId > 0 {
query := m.GetDashboardQuery{Id: dash.FolderId, OrgId: c.OrgId}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Dashboard folder could not be read", err)
}
meta.FolderTitle = query.Result.Title
}
// make sure db version is in sync with json model version
dash.Data.Set("version", dash.Version)
dto := dtos.DashboardFullWithMeta{
Dashboard: dash.Data,
Meta: meta,
} }
// TODO(ben): copy this performance metrics logic for the new API endpoints added
c.TimeRequest(metrics.M_Api_Dashboard_Get) c.TimeRequest(metrics.M_Api_Dashboard_Get)
c.JSON(200, dto) return Json(200, dto)
} }
func getUserLogin(userId int64) string { func getUserLogin(userId int64) string {
@@ -98,24 +124,32 @@ func getUserLogin(userId int64) string {
} }
} }
func DeleteDashboard(c *middleware.Context) { func getDashboardHelper(orgId int64, slug string, id int64) (*m.Dashboard, Response) {
slug := c.Params(":slug") query := m.GetDashboardQuery{Slug: slug, Id: id, OrgId: orgId}
query := m.GetDashboardQuery{Slug: slug, OrgId: c.OrgId}
if err := bus.Dispatch(&query); err != nil { if err := bus.Dispatch(&query); err != nil {
c.JsonApiErr(404, "Dashboard not found", nil) return nil, ApiError(404, "Dashboard not found", err)
return }
return query.Result, nil
} }
cmd := m.DeleteDashboardCommand{Slug: slug, OrgId: c.OrgId} func DeleteDashboard(c *middleware.Context) Response {
dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0)
if rsp != nil {
return rsp
}
guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
cmd := m.DeleteDashboardCommand{OrgId: c.OrgId, Id: dash.Id}
if err := bus.Dispatch(&cmd); err != nil { if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "Failed to delete dashboard", err) return ApiError(500, "Failed to delete dashboard", err)
return
} }
var resp = map[string]interface{}{"title": query.Result.Title} var resp = map[string]interface{}{"title": dash.Title}
return Json(200, resp)
c.JSON(200, resp)
} }
func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response { func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
@@ -124,6 +158,20 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
dash := cmd.GetDashboardModel() dash := cmd.GetDashboardModel()
guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
if dash.IsFolder && dash.FolderId > 0 {
return ApiError(400, m.ErrDashboardFolderCannotHaveParent.Error(), nil)
}
// Check if Title is empty
if dash.Title == "" {
return ApiError(400, m.ErrDashboardTitleEmpty.Error(), nil)
}
if dash.Id == 0 { if dash.Id == 0 {
limitReached, err := middleware.QuotaReached(c, "dashboard") limitReached, err := middleware.QuotaReached(c, "dashboard")
if err != nil { if err != nil {
@@ -139,6 +187,7 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
Message: cmd.Message, Message: cmd.Message,
OrgId: c.OrgId, OrgId: c.OrgId,
UserId: c.UserId, UserId: c.UserId,
Overwrite: cmd.Overwrite,
} }
dashboard, err := dashboards.GetRepository().SaveDashboard(dashItem) dashboard, err := dashboards.GetRepository().SaveDashboard(dashItem)
@@ -177,11 +226,7 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
} }
c.TimeRequest(metrics.M_Api_Dashboard_Save) c.TimeRequest(metrics.M_Api_Dashboard_Save)
return Json(200, util.DynMap{"status": "success", "slug": dashboard.Slug, "version": dashboard.Version}) return Json(200, util.DynMap{"status": "success", "slug": dashboard.Slug, "version": dashboard.Version, "id": dashboard.Id})
}
func canEditDashboard(role m.RoleType) bool {
return role == m.ROLE_ADMIN || role == m.ROLE_EDITOR || role == m.ROLE_READ_ONLY_EDITOR
} }
func GetHomeDashboard(c *middleware.Context) Response { func GetHomeDashboard(c *middleware.Context) Response {
@@ -209,7 +254,9 @@ func GetHomeDashboard(c *middleware.Context) Response {
dash := dtos.DashboardFullWithMeta{} dash := dtos.DashboardFullWithMeta{}
dash.Meta.IsHome = true dash.Meta.IsHome = true
dash.Meta.CanEdit = canEditDashboard(c.OrgRole) dash.Meta.CanEdit = c.SignedInUser.HasRole(m.ROLE_EDITOR)
dash.Meta.FolderTitle = "Root"
jsonParser := json.NewDecoder(file) jsonParser := json.NewDecoder(file)
if err := jsonParser.Decode(&dash.Dashboard); err != nil { if err := jsonParser.Decode(&dash.Dashboard); err != nil {
return ApiError(500, "Failed to load home dashboard", err) return ApiError(500, "Failed to load home dashboard", err)
@@ -223,39 +270,41 @@ func GetHomeDashboard(c *middleware.Context) Response {
} }
func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) { func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) {
rows := dash.Get("rows").MustArray() panels := dash.Get("panels").MustArray()
row := simplejson.NewFromAny(rows[0])
newpanel := simplejson.NewFromAny(map[string]interface{}{ newpanel := simplejson.NewFromAny(map[string]interface{}{
"type": "gettingstarted", "type": "gettingstarted",
"id": 123123, "id": 123123,
"span": 12, "gridPos": map[string]interface{}{
"x": 0,
"y": 3,
"w": 24,
"h": 4,
},
}) })
panels := row.Get("panels").MustArray()
panels = append(panels, newpanel) panels = append(panels, newpanel)
row.Set("panels", panels) dash.Set("panels", panels)
} }
// GetDashboardVersions returns all dashboard versions as JSON // GetDashboardVersions returns all dashboard versions as JSON
func GetDashboardVersions(c *middleware.Context) Response { func GetDashboardVersions(c *middleware.Context) Response {
dashboardId := c.ParamsInt64(":dashboardId") dashId := c.ParamsInt64(":dashboardId")
limit := c.QueryInt("limit")
start := c.QueryInt("start")
if limit == 0 { guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
limit = 1000 if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
} }
query := m.GetDashboardVersionsQuery{ query := m.GetDashboardVersionsQuery{
OrgId: c.OrgId, OrgId: c.OrgId,
DashboardId: dashboardId, DashboardId: dashId,
Limit: limit, Limit: c.QueryInt("limit"),
Start: start, Start: c.QueryInt("start"),
} }
if err := bus.Dispatch(&query); err != nil { if err := bus.Dispatch(&query); err != nil {
return ApiError(404, fmt.Sprintf("No versions found for dashboardId %d", dashboardId), err) return ApiError(404, fmt.Sprintf("No versions found for dashboardId %d", dashId), err)
} }
for _, version := range query.Result { for _, version := range query.Result {
@@ -279,17 +328,21 @@ func GetDashboardVersions(c *middleware.Context) Response {
// GetDashboardVersion returns the dashboard version with the given ID. // GetDashboardVersion returns the dashboard version with the given ID.
func GetDashboardVersion(c *middleware.Context) Response { func GetDashboardVersion(c *middleware.Context) Response {
dashboardId := c.ParamsInt64(":dashboardId") dashId := c.ParamsInt64(":dashboardId")
version := c.ParamsInt(":id")
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
query := m.GetDashboardVersionQuery{ query := m.GetDashboardVersionQuery{
OrgId: c.OrgId, OrgId: c.OrgId,
DashboardId: dashboardId, DashboardId: dashId,
Version: version, Version: c.ParamsInt(":id"),
} }
if err := bus.Dispatch(&query); err != nil { if err := bus.Dispatch(&query); err != nil {
return ApiError(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", version, dashboardId), err) return ApiError(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", query.Version, dashId), err)
} }
creator := "Anonymous" creator := "Anonymous"
@@ -340,19 +393,21 @@ func CalculateDashboardDiff(c *middleware.Context, apiOptions dtos.CalculateDiff
// RestoreDashboardVersion restores a dashboard to the given version. // RestoreDashboardVersion restores a dashboard to the given version.
func RestoreDashboardVersion(c *middleware.Context, apiCmd dtos.RestoreDashboardVersionCommand) Response { func RestoreDashboardVersion(c *middleware.Context, apiCmd dtos.RestoreDashboardVersionCommand) Response {
dashboardId := c.ParamsInt64(":dashboardId") dash, rsp := getDashboardHelper(c.OrgId, "", c.ParamsInt64(":dashboardId"))
if rsp != nil {
dashQuery := m.GetDashboardQuery{Id: dashboardId, OrgId: c.OrgId} return rsp
if err := bus.Dispatch(&dashQuery); err != nil {
return ApiError(404, "Dashboard not found", nil)
} }
versionQuery := m.GetDashboardVersionQuery{DashboardId: dashboardId, Version: apiCmd.Version, OrgId: c.OrgId} guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
versionQuery := m.GetDashboardVersionQuery{DashboardId: dash.Id, Version: apiCmd.Version, OrgId: c.OrgId}
if err := bus.Dispatch(&versionQuery); err != nil { if err := bus.Dispatch(&versionQuery); err != nil {
return ApiError(404, "Dashboard version not found", nil) return ApiError(404, "Dashboard version not found", nil)
} }
dashboard := dashQuery.Result
version := versionQuery.Result version := versionQuery.Result
saveCmd := m.SaveDashboardCommand{} saveCmd := m.SaveDashboardCommand{}
@@ -360,7 +415,7 @@ func RestoreDashboardVersion(c *middleware.Context, apiCmd dtos.RestoreDashboard
saveCmd.OrgId = c.OrgId saveCmd.OrgId = c.OrgId
saveCmd.UserId = c.UserId saveCmd.UserId = c.UserId
saveCmd.Dashboard = version.Data saveCmd.Dashboard = version.Data
saveCmd.Dashboard.Set("version", dashboard.Version) saveCmd.Dashboard.Set("version", dash.Version)
saveCmd.Message = fmt.Sprintf("Restored from version %d", version.Version) saveCmd.Message = fmt.Sprintf("Restored from version %d", version.Version)
return PostDashboard(c, saveCmd) return PostDashboard(c, saveCmd)

79
pkg/api/dashboard_acl.go Normal file
View File

@@ -0,0 +1,79 @@
package api
import (
"time"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/guardian"
)
func GetDashboardAclList(c *middleware.Context) Response {
dashId := c.ParamsInt64(":dashboardId")
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
return dashboardGuardianResponse(err)
}
acl, err := guardian.GetAcl()
if err != nil {
return ApiError(500, "Failed to get dashboard acl", err)
}
return Json(200, acl)
}
func UpdateDashboardAcl(c *middleware.Context, apiCmd dtos.UpdateDashboardAclCommand) Response {
dashId := c.ParamsInt64(":dashboardId")
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
return dashboardGuardianResponse(err)
}
cmd := m.UpdateDashboardAclCommand{}
cmd.DashboardId = dashId
for _, item := range apiCmd.Items {
cmd.Items = append(cmd.Items, &m.DashboardAcl{
OrgId: c.OrgId,
DashboardId: dashId,
UserId: item.UserId,
TeamId: item.TeamId,
Role: item.Role,
Permission: item.Permission,
Created: time.Now(),
Updated: time.Now(),
})
}
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrDashboardAclInfoMissing || err == m.ErrDashboardPermissionDashboardEmpty {
return ApiError(409, err.Error(), err)
}
return ApiError(500, "Failed to create permission", err)
}
return ApiSuccess("Dashboard acl updated")
}
func DeleteDashboardAcl(c *middleware.Context) Response {
dashId := c.ParamsInt64(":dashboardId")
aclId := c.ParamsInt64(":aclId")
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
return dashboardGuardianResponse(err)
}
cmd := m.RemoveDashboardAclCommand{OrgId: c.OrgId, AclId: aclId}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to delete permission for user", err)
}
return Json(200, "")
}

View File

@@ -0,0 +1,174 @@
package api
import (
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestDashboardAclApiEndpoint(t *testing.T) {
Convey("Given a dashboard acl", t, func() {
mockResult := []*m.DashboardAclInfoDTO{
{Id: 1, OrgId: 1, DashboardId: 1, UserId: 2, Permission: m.PERMISSION_VIEW},
{Id: 2, OrgId: 1, DashboardId: 1, UserId: 3, Permission: m.PERMISSION_EDIT},
{Id: 3, OrgId: 1, DashboardId: 1, UserId: 4, Permission: m.PERMISSION_ADMIN},
{Id: 4, OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_VIEW},
{Id: 5, OrgId: 1, DashboardId: 1, TeamId: 2, Permission: m.PERMISSION_ADMIN},
}
dtoRes := transformDashboardAclsToDTOs(mockResult)
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
})
Convey("When user is org admin", func() {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardsId/acl", m.ROLE_ADMIN, func(sc *scenarioContext) {
Convey("Should be able to access ACL", func() {
sc.handlerFunc = GetDashboardAclList
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("When user is editor and has admin permission in the ACL", func() {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, func(sc *scenarioContext) {
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
Convey("Should be able to access ACL", func() {
sc.handlerFunc = GetDashboardAclList
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) {
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error {
return nil
})
Convey("Should be able to delete permission", func() {
sc.handlerFunc = DeleteDashboardAcl
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
})
Convey("When user is a member of a team in the ACL with admin permission", func() {
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardsId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) {
teamResp = append(teamResp, &m.Team{Id: 2, OrgId: 1, Name: "UG2"})
bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error {
return nil
})
Convey("Should be able to delete permission", func() {
sc.handlerFunc = DeleteDashboardAcl
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
})
})
})
Convey("When user is editor and has edit permission in the ACL", func() {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, func(sc *scenarioContext) {
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_EDIT})
Convey("Should not be able to access ACL", func() {
sc.handlerFunc = GetDashboardAclList
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) {
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_EDIT})
bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error {
return nil
})
Convey("Should be not be able to delete permission", func() {
sc.handlerFunc = DeleteDashboardAcl
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
})
})
Convey("When user is editor and not in the ACL", func() {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardsId/acl", m.ROLE_EDITOR, func(sc *scenarioContext) {
Convey("Should not be able to access ACL", func() {
sc.handlerFunc = GetDashboardAclList
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/user/1", "/api/dashboards/id/:dashboardsId/acl/user/:userId", m.ROLE_EDITOR, func(sc *scenarioContext) {
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_VIEW})
bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error {
return nil
})
Convey("Should be not be able to delete permission", func() {
sc.handlerFunc = DeleteDashboardAcl
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
})
})
})
}
func transformDashboardAclsToDTOs(acls []*m.DashboardAclInfoDTO) []*m.DashboardAclInfoDTO {
dtos := make([]*m.DashboardAclInfoDTO, 0)
for _, acl := range acls {
dto := &m.DashboardAclInfoDTO{
Id: acl.Id,
OrgId: acl.OrgId,
DashboardId: acl.DashboardId,
Permission: acl.Permission,
UserId: acl.UserId,
TeamId: acl.TeamId,
}
dtos = append(dtos, dto)
}
return dtos
}

521
pkg/api/dashboard_test.go Normal file
View File

@@ -0,0 +1,521 @@
package api
import (
"encoding/json"
"path/filepath"
"testing"
macaron "gopkg.in/macaron.v1"
"github.com/go-macaron/session"
"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/alerting"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
)
type fakeDashboardRepo struct {
inserted []*dashboards.SaveDashboardItem
getDashboard []*m.Dashboard
}
func (repo *fakeDashboardRepo) SaveDashboard(json *dashboards.SaveDashboardItem) (*m.Dashboard, error) {
repo.inserted = append(repo.inserted, json)
return json.Dashboard, nil
}
var fakeRepo *fakeDashboardRepo
func TestDashboardApiEndpoint(t *testing.T) {
Convey("Given a dashboard with a parent folder which does not have an acl", t, func() {
fakeDash := m.NewDashboard("Child dash")
fakeDash.Id = 1
fakeDash.FolderId = 1
fakeDash.HasAcl = false
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
query.Result = fakeDash
return nil
})
viewerRole := m.ROLE_VIEWER
editorRole := m.ROLE_EDITOR
aclMockResp := []*m.DashboardAclInfoDTO{
{Role: &viewerRole, Permission: m.PERMISSION_VIEW},
{Role: &editorRole, Permission: m.PERMISSION_EDIT},
}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = aclMockResp
return nil
})
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
query.Result = []*m.Team{}
return nil
})
cmd := m.SaveDashboardCommand{
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"folderId": fakeDash.FolderId,
"title": fakeDash.Title,
"id": fakeDash.Id,
}),
}
Convey("When user is an Org Viewer", func() {
role := m.ROLE_VIEWER
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
dash := GetDashboardShouldReturn200(sc)
Convey("Should not be able to edit or save dashboard", func() {
So(dash.Meta.CanEdit, ShouldBeFalse)
So(dash.Meta.CanSave, ShouldBeFalse)
So(dash.Meta.CanAdmin, ShouldBeFalse)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
CallDeleteDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
CallGetDashboardVersion(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
CallGetDashboardVersions(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
})
Convey("When user is an Org Editor", func() {
role := m.ROLE_EDITOR
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
dash := GetDashboardShouldReturn200(sc)
Convey("Should be able to edit or save dashboard", func() {
So(dash.Meta.CanEdit, ShouldBeTrue)
So(dash.Meta.CanSave, ShouldBeTrue)
So(dash.Meta.CanAdmin, ShouldBeFalse)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
CallDeleteDashboard(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
CallGetDashboardVersion(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
CallGetDashboardVersions(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
Convey("When saving a dashboard folder in another folder", func() {
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
query.Result = fakeDash
query.Result.IsFolder = true
return nil
})
invalidCmd := m.SaveDashboardCommand{
FolderId: fakeDash.FolderId,
IsFolder: true,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"folderId": fakeDash.FolderId,
"title": fakeDash.Title,
}),
}
Convey("Should return an error", func() {
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, invalidCmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 400)
})
})
})
})
})
Convey("Given a dashboard with a parent folder which has an acl", t, func() {
fakeDash := m.NewDashboard("Child dash")
fakeDash.Id = 1
fakeDash.FolderId = 1
fakeDash.HasAcl = true
setting.ViewersCanEdit = false
aclMockResp := []*m.DashboardAclInfoDTO{
{
DashboardId: 1,
Permission: m.PERMISSION_EDIT,
UserId: 200,
},
}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = aclMockResp
return nil
})
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
query.Result = fakeDash
return nil
})
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
query.Result = []*m.Team{}
return nil
})
cmd := m.SaveDashboardCommand{
FolderId: fakeDash.FolderId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": fakeDash.Id,
"folderId": fakeDash.FolderId,
"title": fakeDash.Title,
}),
}
Convey("When user is an Org Viewer and has no permissions for this dashboard", func() {
role := m.ROLE_VIEWER
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
sc.handlerFunc = GetDashboard
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
Convey("Should be denied access", func() {
So(sc.resp.Code, ShouldEqual, 403)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
CallDeleteDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
CallGetDashboardVersion(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
CallGetDashboardVersions(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
})
Convey("When user is an Org Editor and has no permissions for this dashboard", func() {
role := m.ROLE_EDITOR
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
sc.handlerFunc = GetDashboard
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
Convey("Should be denied access", func() {
So(sc.resp.Code, ShouldEqual, 403)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
CallDeleteDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
CallGetDashboardVersion(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
CallGetDashboardVersions(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
})
Convey("When user is an Org Viewer but has an edit permission", func() {
role := m.ROLE_VIEWER
mockResult := []*m.DashboardAclInfoDTO{
{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_EDIT},
}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = mockResult
return nil
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
dash := GetDashboardShouldReturn200(sc)
Convey("Should be able to get dashboard with edit rights", func() {
So(dash.Meta.CanEdit, ShouldBeTrue)
So(dash.Meta.CanSave, ShouldBeTrue)
So(dash.Meta.CanAdmin, ShouldBeFalse)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
CallDeleteDashboard(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
CallGetDashboardVersion(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
CallGetDashboardVersions(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
})
Convey("When user is an Org Viewer and viewers can edit", func() {
role := m.ROLE_VIEWER
setting.ViewersCanEdit = true
mockResult := []*m.DashboardAclInfoDTO{
{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW},
}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = mockResult
return nil
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
dash := GetDashboardShouldReturn200(sc)
Convey("Should be able to get dashboard with edit rights but can save should be false", func() {
So(dash.Meta.CanEdit, ShouldBeTrue)
So(dash.Meta.CanSave, ShouldBeFalse)
So(dash.Meta.CanAdmin, ShouldBeFalse)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
CallDeleteDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
})
Convey("When user is an Org Viewer but has an admin permission", func() {
role := m.ROLE_VIEWER
mockResult := []*m.DashboardAclInfoDTO{
{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_ADMIN},
}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = mockResult
return nil
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
dash := GetDashboardShouldReturn200(sc)
Convey("Should be able to get dashboard with edit rights", func() {
So(dash.Meta.CanEdit, ShouldBeTrue)
So(dash.Meta.CanSave, ShouldBeTrue)
So(dash.Meta.CanAdmin, ShouldBeTrue)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
CallDeleteDashboard(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
CallGetDashboardVersion(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
CallGetDashboardVersions(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
})
Convey("When user is an Org Editor but has a view permission", func() {
role := m.ROLE_EDITOR
mockResult := []*m.DashboardAclInfoDTO{
{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW},
}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = mockResult
return nil
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
dash := GetDashboardShouldReturn200(sc)
Convey("Should not be able to edit or save dashboard", func() {
So(dash.Meta.CanEdit, ShouldBeFalse)
So(dash.Meta.CanSave, ShouldBeFalse)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
CallDeleteDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
CallGetDashboardVersion(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
CallGetDashboardVersions(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
})
})
}
func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta {
sc.handlerFunc = GetDashboard
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
dash := dtos.DashboardFullWithMeta{}
err := json.NewDecoder(sc.resp.Body).Decode(&dash)
So(err, ShouldBeNil)
return dash
}
func CallGetDashboardVersion(sc *scenarioContext) {
bus.AddHandler("test", func(query *m.GetDashboardVersionQuery) error {
query.Result = &m.DashboardVersion{}
return nil
})
sc.handlerFunc = GetDashboardVersion
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
}
func CallGetDashboardVersions(sc *scenarioContext) {
bus.AddHandler("test", func(query *m.GetDashboardVersionsQuery) error {
query.Result = []*m.DashboardVersionDTO{}
return nil
})
sc.handlerFunc = GetDashboardVersions
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
}
func CallDeleteDashboard(sc *scenarioContext) {
bus.AddHandler("test", func(cmd *m.DeleteDashboardCommand) error {
return nil
})
sc.handlerFunc = DeleteDashboard
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
}
func CallPostDashboard(sc *scenarioContext) {
bus.AddHandler("test", func(cmd *alerting.ValidateDashboardAlertsCommand) error {
return nil
})
bus.AddHandler("test", func(cmd *m.SaveDashboardCommand) error {
cmd.Result = &m.Dashboard{Id: 2, Slug: "Dash", Version: 2}
return nil
})
bus.AddHandler("test", func(cmd *alerting.UpdateDashboardAlertsCommand) error {
return nil
})
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
}
func postDashboardScenario(desc string, url string, routePattern string, role m.RoleType, cmd m.SaveDashboardCommand, fn scenarioFunc) {
Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers()
sc := &scenarioContext{
url: url,
}
viewsPath, _ := filepath.Abs("../../public/views")
sc.m = macaron.New()
sc.m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: viewsPath,
Delims: macaron.Delims{Left: "[[", Right: "]]"},
}))
sc.m.Use(middleware.GetContextHandler())
sc.m.Use(middleware.Sessioner(&session.Options{}))
sc.defaultHandler = wrap(func(c *middleware.Context) Response {
sc.context = c
sc.context.UserId = TestUserID
sc.context.OrgId = TestOrgID
sc.context.OrgRole = role
return PostDashboard(c, cmd)
})
fakeRepo = &fakeDashboardRepo{}
dashboards.SetRepository(fakeRepo)
sc.m.Post(routePattern, sc.defaultHandler)
fn(sc)
})
}

View File

@@ -56,6 +56,10 @@ func TestDataSourcesProxy(t *testing.T) {
} }
func loggedInUserScenario(desc string, url string, fn scenarioFunc) { func loggedInUserScenario(desc string, url string, fn scenarioFunc) {
loggedInUserScenarioWithRole(desc, "GET", url, url, models.ROLE_EDITOR, fn)
}
func loggedInUserScenarioWithRole(desc string, method string, url string, routePattern string, role models.RoleType, fn scenarioFunc) {
Convey(desc+" "+url, func() { Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers() defer bus.ClearBusHandlers()
@@ -77,7 +81,7 @@ func loggedInUserScenario(desc string, url string, fn scenarioFunc) {
sc.context = c sc.context = c
sc.context.UserId = TestUserID sc.context.UserId = TestUserID
sc.context.OrgId = TestOrgID sc.context.OrgId = TestOrgID
sc.context.OrgRole = models.ROLE_EDITOR sc.context.OrgRole = role
if sc.handlerFunc != nil { if sc.handlerFunc != nil {
return sc.handlerFunc(sc.context) return sc.handlerFunc(sc.context)
} }
@@ -85,7 +89,12 @@ func loggedInUserScenario(desc string, url string, fn scenarioFunc) {
return nil return nil
}) })
sc.m.Get(url, sc.defaultHandler) switch method {
case "GET":
sc.m.Get(routePattern, sc.defaultHandler)
case "DELETE":
sc.m.Delete(routePattern, sc.defaultHandler)
}
fn(sc) fn(sc)
}) })

16
pkg/api/dtos/acl.go Normal file
View File

@@ -0,0 +1,16 @@
package dtos
import (
m "github.com/grafana/grafana/pkg/models"
)
type UpdateDashboardAclCommand struct {
Items []DashboardAclUpdateItem `json:"items"`
}
type DashboardAclUpdateItem struct {
UserId int64 `json:"userId"`
TeamId int64 `json:"teamId"`
Role *m.RoleType `json:"role,omitempty"`
Permission m.PermissionType `json:"permission"`
}

View File

@@ -13,6 +13,7 @@ type DashboardMeta struct {
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
CanSave bool `json:"canSave"` CanSave bool `json:"canSave"`
CanEdit bool `json:"canEdit"` CanEdit bool `json:"canEdit"`
CanAdmin bool `json:"canAdmin"`
CanStar bool `json:"canStar"` CanStar bool `json:"canStar"`
Slug string `json:"slug"` Slug string `json:"slug"`
Expires time.Time `json:"expires"` Expires time.Time `json:"expires"`
@@ -21,6 +22,10 @@ type DashboardMeta struct {
UpdatedBy string `json:"updatedBy"` UpdatedBy string `json:"updatedBy"`
CreatedBy string `json:"createdBy"` CreatedBy string `json:"createdBy"`
Version int `json:"version"` Version int `json:"version"`
HasAcl bool `json:"hasAcl"`
IsFolder bool `json:"isFolder"`
FolderId int64 `json:"folderId"`
FolderTitle string `json:"folderTitle"`
} }
type DashboardFullWithMeta struct { type DashboardFullWithMeta struct {

View File

@@ -7,9 +7,10 @@ type IndexViewData struct {
AppSubUrl string AppSubUrl string
GoogleAnalyticsId string GoogleAnalyticsId string
GoogleTagManagerId string GoogleTagManagerId string
MainNavLinks []*NavLink NavTree []*NavLink
BuildVersion string BuildVersion string
BuildCommit string BuildCommit string
Theme string
NewGrafanaVersionExists bool NewGrafanaVersionExists bool
NewGrafanaVersion string NewGrafanaVersion string
} }
@@ -20,10 +21,16 @@ type PluginCss struct {
} }
type NavLink struct { type NavLink struct {
Id string `json:"id,omitempty"`
Text string `json:"text,omitempty"` Text string `json:"text,omitempty"`
Description string `json:"description,omitempty"`
SubTitle string `json:"subTitle,omitempty"`
Icon string `json:"icon,omitempty"` Icon string `json:"icon,omitempty"`
Img string `json:"img,omitempty"` Img string `json:"img,omitempty"`
Url string `json:"url,omitempty"` Url string `json:"url,omitempty"`
Target string `json:"target,omitempty"`
Divider bool `json:"divider,omitempty"` Divider bool `json:"divider,omitempty"`
HideFromMenu bool `json:"hideFromMenu,omitempty"`
HideFromTabs bool `json:"hideFromTabs,omitempty"`
Children []*NavLink `json:"children,omitempty"` Children []*NavLink `json:"children,omitempty"`
} }

View File

@@ -6,7 +6,7 @@ type AddInviteForm struct {
LoginOrEmail string `json:"loginOrEmail" binding:"Required"` LoginOrEmail string `json:"loginOrEmail" binding:"Required"`
Name string `json:"name"` Name string `json:"name"`
Role m.RoleType `json:"role" binding:"Required"` Role m.RoleType `json:"role" binding:"Required"`
SkipEmails bool `json:"skipEmails"` SendEmail bool `json:"sendEmail"`
} }
type InviteInfo struct { type InviteInfo struct {

View File

@@ -27,6 +27,7 @@ type CurrentUser struct {
Email string `json:"email"` Email string `json:"email"`
Name string `json:"name"` Name string `json:"name"`
LightTheme bool `json:"lightTheme"` LightTheme bool `json:"lightTheme"`
OrgCount int `json:"orgCount"`
OrgId int64 `json:"orgId"` OrgId int64 `json:"orgId"`
OrgName string `json:"orgName"` OrgName string `json:"orgName"`
OrgRole m.RoleType `json:"orgRole"` OrgRole m.RoleType `json:"orgRole"`

View File

@@ -143,7 +143,6 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
"alertingEnabled": setting.AlertingEnabled, "alertingEnabled": setting.AlertingEnabled,
"googleAnalyticsId": setting.GoogleAnalyticsId, "googleAnalyticsId": setting.GoogleAnalyticsId,
"disableLoginForm": setting.DisableLoginForm, "disableLoginForm": setting.DisableLoginForm,
"disableSignoutMenu": setting.DisableSignoutMenu,
"externalUserMngInfo": setting.ExternalUserMngInfo, "externalUserMngInfo": setting.ExternalUserMngInfo,
"externalUserMngLinkUrl": setting.ExternalUserMngLinkUrl, "externalUserMngLinkUrl": setting.ExternalUserMngLinkUrl,
"externalUserMngLinkName": setting.ExternalUserMngLinkName, "externalUserMngLinkName": setting.ExternalUserMngLinkName,

View File

@@ -50,6 +50,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
Login: c.Login, Login: c.Login,
Email: c.Email, Email: c.Email,
Name: c.Name, Name: c.Name,
OrgCount: c.OrgCount,
OrgId: c.OrgId, OrgId: c.OrgId,
OrgName: c.OrgName, OrgName: c.OrgName,
OrgRole: c.OrgRole, OrgRole: c.OrgRole,
@@ -61,6 +62,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
HelpFlags1: c.HelpFlags1, HelpFlags1: c.HelpFlags1,
}, },
Settings: settings, Settings: settings,
Theme: prefs.Theme,
AppUrl: appUrl, AppUrl: appUrl,
AppSubUrl: appSubUrl, AppSubUrl: appSubUrl,
GoogleAnalyticsId: setting.GoogleAnalyticsId, GoogleAnalyticsId: setting.GoogleAnalyticsId,
@@ -82,55 +84,80 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
themeUrlParam := c.Query("theme") themeUrlParam := c.Query("theme")
if themeUrlParam == "light" { if themeUrlParam == "light" {
data.User.LightTheme = true data.User.LightTheme = true
} data.Theme = "light"
dashboardChildNavs := []*dtos.NavLink{
{Text: "Home", Url: setting.AppSubUrl + "/"},
{Text: "Playlists", Url: setting.AppSubUrl + "/playlists"},
{Text: "Snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots"},
} }
if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR { if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR {
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Divider: true}) data.NavTree = append(data.NavTree, &dtos.NavLink{
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "New", Icon: "fa fa-plus", Url: setting.AppSubUrl + "/dashboard/new"}) Text: "Create",
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "Import", Icon: "fa fa-download", Url: setting.AppSubUrl + "/dashboard/new/?editview=import"}) Id: "create",
Icon: "fa fa-fw fa-plus",
Url: setting.AppSubUrl + "dashboard/new",
Children: []*dtos.NavLink{
{Text: "Dashboard", Icon: "gicon gicon-dashboard-new", Url: setting.AppSubUrl + "/dashboard/new"},
{Text: "Folder", SubTitle: "Create a new folder to organize your dashboards", Id: "folder", Icon: "gicon gicon-folder-new", Url: setting.AppSubUrl + "/dashboards/folder/new"},
{Text: "Import", SubTitle: "Import dashboard from file or Grafana.com", Id: "import", Icon: "gicon gicon-dashboard-import", Url: setting.AppSubUrl + "/dashboard/import"},
},
})
} }
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ dashboardChildNavs := []*dtos.NavLink{
{Text: "Home", Url: setting.AppSubUrl + "/", Icon: "gicon gicon-home", HideFromTabs: true},
{Divider: true, HideFromTabs: true},
{Text: "Manage", Id: "manage-dashboards", Url: setting.AppSubUrl + "/dashboards", Icon: "gicon gicon-manage"},
{Text: "Playlists", Id: "playlists", Url: setting.AppSubUrl + "/playlists", Icon: "gicon gicon-playlists"},
{Text: "Snapshots", Id: "snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots", Icon: "gicon gicon-snapshots"},
}
data.NavTree = append(data.NavTree, &dtos.NavLink{
Text: "Dashboards", Text: "Dashboards",
Icon: "icon-gf icon-gf-dashboard", Id: "dashboards",
SubTitle: "Manage dashboards & folders",
Icon: "gicon gicon-dashboard",
Url: setting.AppSubUrl + "/", Url: setting.AppSubUrl + "/",
Children: dashboardChildNavs, Children: dashboardChildNavs,
}) })
if c.IsSignedIn {
profileNode := &dtos.NavLink{
Text: c.SignedInUser.NameOrFallback(),
SubTitle: c.SignedInUser.Login,
Id: "profile",
Img: data.User.GravatarUrl,
Url: setting.AppSubUrl + "/profile",
HideFromMenu: true,
Children: []*dtos.NavLink{
{Text: "Preferences", Id: "profile-settings", Url: setting.AppSubUrl + "/profile", Icon: "gicon gicon-preferences"},
{Text: "Change Password", Id: "change-password", Url: setting.AppSubUrl + "/profile/password", Icon: "fa fa-fw fa-lock", HideFromMenu: true},
},
}
if !setting.DisableSignoutMenu {
// add sign out first
profileNode.Children = append(profileNode.Children, &dtos.NavLink{
Text: "Sign out", Id: "sign-out", Url: setting.AppSubUrl + "/logout", Icon: "fa fa-fw fa-sign-out", Target: "_self",
})
}
data.NavTree = append(data.NavTree, profileNode)
}
if setting.AlertingEnabled && (c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR) { if setting.AlertingEnabled && (c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR) {
alertChildNavs := []*dtos.NavLink{ alertChildNavs := []*dtos.NavLink{
{Text: "Alert List", Url: setting.AppSubUrl + "/alerting/list"}, {Text: "Alert Rules", Id: "alert-list", Url: setting.AppSubUrl + "/alerting/list", Icon: "gicon gicon-alert-rules"},
{Text: "Notification channels", Url: setting.AppSubUrl + "/alerting/notifications"}, {Text: "Notification channels", Id: "channels", Url: setting.AppSubUrl + "/alerting/notifications", Icon: "gicon gicon-alert-notification-channel"},
} }
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ data.NavTree = append(data.NavTree, &dtos.NavLink{
Text: "Alerting", Text: "Alerting",
Icon: "icon-gf icon-gf-alert", SubTitle: "Alert rules & notifications",
Id: "alerting",
Icon: "gicon gicon-alert",
Url: setting.AppSubUrl + "/alerting/list", Url: setting.AppSubUrl + "/alerting/list",
Children: alertChildNavs, Children: alertChildNavs,
}) })
} }
if c.OrgRole == m.ROLE_ADMIN {
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
Text: "Data Sources",
Icon: "icon-gf icon-gf-datasources",
Url: setting.AppSubUrl + "/datasources",
})
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
Text: "Plugins",
Icon: "icon-gf icon-gf-apps",
Url: setting.AppSubUrl + "/plugins",
})
}
enabledPlugins, err := plugins.GetEnabledPlugins(c.OrgId) enabledPlugins, err := plugins.GetEnabledPlugins(c.OrgId)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -140,6 +167,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
if plugin.Pinned { if plugin.Pinned {
appLink := &dtos.NavLink{ appLink := &dtos.NavLink{
Text: plugin.Name, Text: plugin.Name,
Id: "plugin-page-" + plugin.Id,
Url: plugin.DefaultNavUrl, Url: plugin.DefaultNavUrl,
Img: plugin.Info.Logos.Small, Img: plugin.Info.Logos.Small,
} }
@@ -168,29 +196,106 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
if len(appLink.Children) > 0 && c.OrgRole == m.ROLE_ADMIN { if len(appLink.Children) > 0 && c.OrgRole == m.ROLE_ADMIN {
appLink.Children = append(appLink.Children, &dtos.NavLink{Divider: true}) appLink.Children = append(appLink.Children, &dtos.NavLink{Divider: true})
appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Plugin Config", Icon: "fa fa-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit"}) appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Plugin Config", Icon: "gicon gicon-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit"})
} }
if len(appLink.Children) > 0 { if len(appLink.Children) > 0 {
data.MainNavLinks = append(data.MainNavLinks, appLink) data.NavTree = append(data.NavTree, appLink)
} }
} }
} }
if c.IsGrafanaAdmin { if c.OrgRole == m.ROLE_ADMIN {
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ cfgNode := &dtos.NavLink{
Text: "Admin", Id: "cfg",
Icon: "fa fa-fw fa-cogs", Text: "Configuration",
Url: setting.AppSubUrl + "/admin", SubTitle: "Organization: " + c.OrgName,
Icon: "gicon gicon-cog",
Url: setting.AppSubUrl + "/datasources",
Children: []*dtos.NavLink{ Children: []*dtos.NavLink{
{Text: "Global Users", Url: setting.AppSubUrl + "/admin/users"}, {
{Text: "Global Orgs", Url: setting.AppSubUrl + "/admin/orgs"}, Text: "Data Sources",
{Text: "Server Settings", Url: setting.AppSubUrl + "/admin/settings"}, Icon: "gicon gicon-datasources",
{Text: "Server Stats", Url: setting.AppSubUrl + "/admin/stats"}, Description: "Add and configure data sources",
Id: "datasources",
Url: setting.AppSubUrl + "/datasources",
},
{
Text: "Users",
Id: "users",
Description: "Manage org members",
Icon: "gicon gicon-user",
Url: setting.AppSubUrl + "/org/users",
},
{
Text: "Teams",
Id: "teams",
Description: "Manage org groups",
Icon: "gicon gicon-team",
Url: setting.AppSubUrl + "/org/teams",
},
{
Text: "Plugins",
Id: "plugins",
Description: "View and configure plugins",
Icon: "gicon gicon-plugins",
Url: setting.AppSubUrl + "/plugins",
},
{
Text: "Preferences",
Id: "org-settings",
Description: "Organization preferences",
Icon: "gicon gicon-preferences",
Url: setting.AppSubUrl + "/org",
},
{
Text: "API Keys",
Id: "apikeys",
Description: "Create & manage API keys",
Icon: "gicon gicon-apikeys",
Url: setting.AppSubUrl + "/org/apikeys",
},
},
}
if c.IsGrafanaAdmin {
cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{
Divider: true, HideFromTabs: true,
})
cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{
Text: "Server Admin",
HideFromTabs: true,
SubTitle: "Manage all users & orgs",
Id: "admin",
Icon: "gicon gicon-shield",
Url: setting.AppSubUrl + "/admin/users",
Children: []*dtos.NavLink{
{Text: "Users", Id: "global-users", Url: setting.AppSubUrl + "/admin/users", Icon: "gicon gicon-user"},
{Text: "Orgs", Id: "global-orgs", Url: setting.AppSubUrl + "/admin/orgs", Icon: "gicon gicon-org"},
{Text: "Settings", Id: "server-settings", Url: setting.AppSubUrl + "/admin/settings", Icon: "gicon gicon-preferences"},
{Text: "Stats", Id: "server-stats", Url: setting.AppSubUrl + "/admin/stats", Icon: "fa fa-fw fa-bar-chart"},
{Text: "Style Guide", Id: "styleguide", Url: setting.AppSubUrl + "/styleguide", Icon: "fa fa-fw fa-eyedropper"},
}, },
}) })
} }
data.NavTree = append(data.NavTree, cfgNode)
}
data.NavTree = append(data.NavTree, &dtos.NavLink{
Text: "Help",
Id: "help",
Url: "#",
Icon: "gicon gicon-question",
HideFromMenu: true,
Children: []*dtos.NavLink{
{Text: "Keyboard shortcuts", Url: "/shortcuts", Icon: "fa fa-fw fa-keyboard-o", Target: "_self"},
{Text: "Community site", Url: "http://community.grafana.com", Icon: "fa fa-fw fa-comment", Target: "_blank"},
{Text: "Documentation", Url: "http://docs.grafana.org", Icon: "fa fa-fw fa-file", Target: "_blank"},
},
})
return &data, nil return &data, nil
} }

View File

@@ -61,7 +61,7 @@ func AddOrgInvite(c *middleware.Context, inviteDto dtos.AddInviteForm) Response
} }
// send invite email // send invite email
if !inviteDto.SkipEmails && util.IsEmail(inviteDto.LoginOrEmail) { if inviteDto.SendEmail && util.IsEmail(inviteDto.LoginOrEmail) {
emailCmd := m.SendEmailCommand{ emailCmd := m.SendEmailCommand{
To: []string{inviteDto.LoginOrEmail}, To: []string{inviteDto.LoginOrEmail},
Template: "new_user_invite.html", Template: "new_user_invite.html",
@@ -99,7 +99,7 @@ func inviteExistingUserToOrg(c *middleware.Context, user *m.User, inviteDto *dto
return ApiError(500, "Error while trying to create org user", err) return ApiError(500, "Error while trying to create org user", err)
} else { } else {
if !inviteDto.SkipEmails && util.IsEmail(user.Email) { if inviteDto.SendEmail && util.IsEmail(user.Email) {
emailCmd := m.SendEmailCommand{ emailCmd := m.SendEmailCommand{
To: []string{user.Email}, To: []string{user.Email},
Template: "invited_to_org.html", Template: "invited_to_org.html",

View File

@@ -1,6 +1,7 @@
package api package api
import ( import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
@@ -31,10 +32,6 @@ func addOrgUserHelper(cmd m.AddOrgUserCommand) Response {
userToAdd := userQuery.Result userToAdd := userQuery.Result
// if userToAdd.Id == c.UserId {
// return ApiError(400, "Cannot add yourself as user", nil)
// }
cmd.UserId = userToAdd.Id cmd.UserId = userToAdd.Id
if err := bus.Dispatch(&cmd); err != nil { if err := bus.Dispatch(&cmd); err != nil {
@@ -64,6 +61,10 @@ func getOrgUsersHelper(orgId int64) Response {
return ApiError(500, "Failed to get account user", err) return ApiError(500, "Failed to get account user", err)
} }
for _, user := range query.Result {
user.AvatarUrl = dtos.GetGravatarUrl(user.Email)
}
return Json(200, query.Result) return Json(200, query.Result)
} }

View File

@@ -130,7 +130,7 @@ func GetPlaylistItems(c *middleware.Context) Response {
func GetPlaylistDashboards(c *middleware.Context) Response { func GetPlaylistDashboards(c *middleware.Context) Response {
playlistId := c.ParamsInt64(":id") playlistId := c.ParamsInt64(":id")
playlists, err := LoadPlaylistDashboards(c.OrgId, c.UserId, playlistId) playlists, err := LoadPlaylistDashboards(c.OrgId, c.SignedInUser, playlistId)
if err != nil { if err != nil {
return ApiError(500, "Could not load dashboards", err) return ApiError(500, "Could not load dashboards", err)
} }

View File

@@ -34,7 +34,7 @@ func populateDashboardsById(dashboardByIds []int64, dashboardIdOrder map[int64]i
return result, nil return result, nil
} }
func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string, dashboardTagOrder map[string]int) dtos.PlaylistDashboardsSlice { func populateDashboardsByTag(orgId int64, signedInUser *m.SignedInUser, dashboardByTag []string, dashboardTagOrder map[string]int) dtos.PlaylistDashboardsSlice {
result := make(dtos.PlaylistDashboardsSlice, 0) result := make(dtos.PlaylistDashboardsSlice, 0)
if len(dashboardByTag) > 0 { if len(dashboardByTag) > 0 {
@@ -42,7 +42,7 @@ func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string, dashb
searchQuery := search.Query{ searchQuery := search.Query{
Title: "", Title: "",
Tags: []string{tag}, Tags: []string{tag},
UserId: userId, SignedInUser: signedInUser,
Limit: 100, Limit: 100,
IsStarred: false, IsStarred: false,
OrgId: orgId, OrgId: orgId,
@@ -64,7 +64,7 @@ func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string, dashb
return result return result
} }
func LoadPlaylistDashboards(orgId, userId, playlistId int64) (dtos.PlaylistDashboardsSlice, error) { func LoadPlaylistDashboards(orgId int64, signedInUser *m.SignedInUser, playlistId int64) (dtos.PlaylistDashboardsSlice, error) {
playlistItems, _ := LoadPlaylistItems(playlistId) playlistItems, _ := LoadPlaylistItems(playlistId)
dashboardByIds := make([]int64, 0) dashboardByIds := make([]int64, 0)
@@ -89,7 +89,7 @@ func LoadPlaylistDashboards(orgId, userId, playlistId int64) (dtos.PlaylistDashb
var k, _ = populateDashboardsById(dashboardByIds, dashboardIdOrder) var k, _ = populateDashboardsById(dashboardByIds, dashboardIdOrder)
result = append(result, k...) result = append(result, k...)
result = append(result, populateDashboardsByTag(orgId, userId, dashboardByTag, dashboardTagOrder)...) result = append(result, populateDashboardsByTag(orgId, signedInUser, dashboardByTag, dashboardTagOrder)...)
sort.Sort(result) sort.Sort(result)
return result, nil return result, nil

View File

@@ -135,9 +135,24 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
req.Header.Add("Authorization", dsAuth) req.Header.Add("Authorization", dsAuth)
} }
// clear cookie headers // clear cookie header, except for whitelisted cookies
var keptCookies []*http.Cookie
if proxy.ds.JsonData != nil {
if keepCookies := proxy.ds.JsonData.Get("keepCookies"); keepCookies != nil {
keepCookieNames := keepCookies.MustStringArray()
for _, c := range req.Cookies() {
for _, v := range keepCookieNames {
if c.Name == v {
keptCookies = append(keptCookies, c)
}
}
}
}
}
req.Header.Del("Cookie") req.Header.Del("Cookie")
req.Header.Del("Set-Cookie") for _, c := range keptCookies {
req.AddCookie(c)
}
// clear X-Forwarded Host/Port/Proto headers // clear X-Forwarded Host/Port/Proto headers
req.Header.Del("X-Forwarded-Host") req.Header.Del("X-Forwarded-Host")

View File

@@ -149,6 +149,58 @@ func TestDSRouteRule(t *testing.T) {
}) })
}) })
Convey("When proxying a data source with no keepCookies specified", func() {
plugin := &plugins.DataSourcePlugin{}
json, _ := simplejson.NewJson([]byte(`{"keepCookies": []}`))
ds := &m.DataSource{
Type: m.DS_GRAPHITE,
Url: "http://graphite:8086",
JsonData: json,
}
ctx := &middleware.Context{}
proxy := NewDataSourceProxy(ds, plugin, ctx, "")
requestUrl, _ := url.Parse("http://grafana.com/sub")
req := http.Request{URL: requestUrl, Header: make(http.Header)}
cookies := "grafana_user=admin; grafana_remember=99; grafana_sess=11; JSESSION_ID=test"
req.Header.Set("Cookie", cookies)
proxy.getDirector()(&req)
Convey("Should clear all cookies", func() {
So(req.Header.Get("Cookie"), ShouldEqual, "")
})
})
Convey("When proxying a data source with keep cookies specified", func() {
plugin := &plugins.DataSourcePlugin{}
json, _ := simplejson.NewJson([]byte(`{"keepCookies": ["JSESSION_ID"]}`))
ds := &m.DataSource{
Type: m.DS_GRAPHITE,
Url: "http://graphite:8086",
JsonData: json,
}
ctx := &middleware.Context{}
proxy := NewDataSourceProxy(ds, plugin, ctx, "")
requestUrl, _ := url.Parse("http://grafana.com/sub")
req := http.Request{URL: requestUrl, Header: make(http.Header)}
cookies := "grafana_user=admin; grafana_remember=99; grafana_sess=11; JSESSION_ID=test"
req.Header.Set("Cookie", cookies)
proxy.getDirector()(&req)
Convey("Should keep named cookies", func() {
So(req.Header.Get("Cookie"), ShouldEqual, "JSESSION_ID=test")
})
})
Convey("When interpolating string", func() { Convey("When interpolating string", func() {
data := templateData{ data := templateData{
SecureJsonData: map[string]string{ SecureJsonData: map[string]string{

View File

@@ -21,9 +21,12 @@ func RenderToPng(c *middleware.Context) {
Path: c.Params("*") + queryParams, Path: c.Params("*") + queryParams,
Width: queryReader.Get("width", "800"), Width: queryReader.Get("width", "800"),
Height: queryReader.Get("height", "400"), Height: queryReader.Get("height", "400"),
OrgId: c.OrgId,
Timeout: queryReader.Get("timeout", "60"), Timeout: queryReader.Get("timeout", "60"),
OrgId: c.OrgId,
UserId: c.UserId,
OrgRole: c.OrgRole,
Timezone: queryReader.Get("tz", ""), Timezone: queryReader.Get("tz", ""),
Encoding: queryReader.Get("encoding", ""),
} }
pngPath, err := renderer.RenderToPng(renderOpts) pngPath, err := renderer.RenderToPng(renderOpts)

View File

@@ -14,27 +14,38 @@ func Search(c *middleware.Context) {
tags := c.QueryStrings("tag") tags := c.QueryStrings("tag")
starred := c.Query("starred") starred := c.Query("starred")
limit := c.QueryInt("limit") limit := c.QueryInt("limit")
dashboardType := c.Query("type")
if limit == 0 { if limit == 0 {
limit = 1000 limit = 1000
} }
dbids := make([]int, 0) dbids := make([]int64, 0)
for _, id := range c.QueryStrings("dashboardIds") { for _, id := range c.QueryStrings("dashboardIds") {
dashboardId, err := strconv.Atoi(id) dashboardId, err := strconv.ParseInt(id, 10, 64)
if err == nil { if err == nil {
dbids = append(dbids, dashboardId) dbids = append(dbids, dashboardId)
} }
} }
folderIds := make([]int64, 0)
for _, id := range c.QueryStrings("folderIds") {
folderId, err := strconv.ParseInt(id, 10, 64)
if err == nil {
folderIds = append(folderIds, folderId)
}
}
searchQuery := search.Query{ searchQuery := search.Query{
Title: query, Title: query,
Tags: tags, Tags: tags,
UserId: c.UserId, SignedInUser: c.SignedInUser,
Limit: limit, Limit: limit,
IsStarred: starred == "true", IsStarred: starred == "true",
OrgId: c.OrgId, OrgId: c.OrgId,
DashboardIds: dbids, DashboardIds: dbids,
Type: dashboardType,
FolderIds: folderIds,
} }
err := bus.Dispatch(&searchQuery) err := bus.Dispatch(&searchQuery)

92
pkg/api/team.go Normal file
View File

@@ -0,0 +1,92 @@
package api
import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util"
)
// POST /api/teams
func CreateTeam(c *middleware.Context, cmd m.CreateTeamCommand) Response {
cmd.OrgId = c.OrgId
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrTeamNameTaken {
return ApiError(409, "Team name taken", err)
}
return ApiError(500, "Failed to create Team", err)
}
return Json(200, &util.DynMap{
"teamId": cmd.Result.Id,
"message": "Team created",
})
}
// PUT /api/teams/:teamId
func UpdateTeam(c *middleware.Context, cmd m.UpdateTeamCommand) Response {
cmd.Id = c.ParamsInt64(":teamId")
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrTeamNameTaken {
return ApiError(400, "Team name taken", err)
}
return ApiError(500, "Failed to update Team", err)
}
return ApiSuccess("Team updated")
}
// DELETE /api/teams/:teamId
func DeleteTeamById(c *middleware.Context) Response {
if err := bus.Dispatch(&m.DeleteTeamCommand{Id: c.ParamsInt64(":teamId")}); err != nil {
if err == m.ErrTeamNotFound {
return ApiError(404, "Failed to delete Team. ID not found", nil)
}
return ApiError(500, "Failed to update Team", err)
}
return ApiSuccess("Team deleted")
}
// GET /api/teams/search
func SearchTeams(c *middleware.Context) Response {
perPage := c.QueryInt("perpage")
if perPage <= 0 {
perPage = 1000
}
page := c.QueryInt("page")
if page < 1 {
page = 1
}
query := m.SearchTeamsQuery{
Query: c.Query("query"),
Name: c.Query("name"),
Page: page,
Limit: perPage,
OrgId: c.OrgId,
}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to search Teams", err)
}
query.Result.Page = page
query.Result.PerPage = perPage
return Json(200, query.Result)
}
// GET /api/teams/:teamId
func GetTeamById(c *middleware.Context) Response {
query := m.GetTeamByIdQuery{Id: c.ParamsInt64(":teamId")}
if err := bus.Dispatch(&query); err != nil {
if err == m.ErrTeamNotFound {
return ApiError(404, "Team not found", err)
}
return ApiError(500, "Failed to get Team", err)
}
return Json(200, &query.Result)
}

44
pkg/api/team_members.go Normal file
View File

@@ -0,0 +1,44 @@
package api
import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util"
)
// GET /api/teams/:teamId/members
func GetTeamMembers(c *middleware.Context) Response {
query := m.GetTeamMembersQuery{TeamId: c.ParamsInt64(":teamId")}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to get Team Members", err)
}
return Json(200, query.Result)
}
// POST /api/teams/:teamId/members
func AddTeamMember(c *middleware.Context, cmd m.AddTeamMemberCommand) Response {
cmd.TeamId = c.ParamsInt64(":teamId")
cmd.OrgId = c.OrgId
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrTeamMemberAlreadyAdded {
return ApiError(400, "User is already added to this team", err)
}
return ApiError(500, "Failed to add Member to Team", err)
}
return Json(200, &util.DynMap{
"message": "Member added to Team",
})
}
// DELETE /api/teams/:teamId/members/:userId
func RemoveTeamMember(c *middleware.Context) Response {
if err := bus.Dispatch(&m.RemoveTeamMemberCommand{TeamId: c.ParamsInt64(":teamId"), UserId: c.ParamsInt64(":userId")}); err != nil {
return ApiError(500, "Failed to remove Member from Team", err)
}
return ApiSuccess("Team Member removed")
}

71
pkg/api/team_test.go Normal file
View File

@@ -0,0 +1,71 @@
package api
import (
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestTeamApiEndpoint(t *testing.T) {
Convey("Given two teams", t, func() {
mockResult := models.SearchTeamQueryResult{
Teams: []*models.SearchTeamDto{
{Name: "team1"},
{Name: "team2"},
},
TotalCount: 2,
}
Convey("When searching with no parameters", func() {
loggedInUserScenario("When calling GET on", "/api/teams/search", func(sc *scenarioContext) {
var sentLimit int
var sendPage int
bus.AddHandler("test", func(query *models.SearchTeamsQuery) error {
query.Result = mockResult
sentLimit = query.Limit
sendPage = query.Page
return nil
})
sc.handlerFunc = SearchTeams
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
So(sentLimit, ShouldEqual, 1000)
So(sendPage, ShouldEqual, 1)
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
So(err, ShouldBeNil)
So(respJSON.Get("totalCount").MustInt(), ShouldEqual, 2)
So(len(respJSON.Get("teams").MustArray()), ShouldEqual, 2)
})
})
Convey("When searching with page and perpage parameters", func() {
loggedInUserScenario("When calling GET on", "/api/teams/search", func(sc *scenarioContext) {
var sentLimit int
var sendPage int
bus.AddHandler("test", func(query *models.SearchTeamsQuery) error {
query.Result = mockResult
sentLimit = query.Limit
sendPage = query.Page
return nil
})
sc.handlerFunc = SearchTeams
sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec()
So(sentLimit, ShouldEqual, 10)
So(sendPage, ShouldEqual, 2)
})
})
})
}

View File

@@ -1,6 +1,7 @@
package api package api
import ( import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
@@ -219,7 +220,7 @@ func SearchUsers(c *middleware.Context) Response {
return Json(200, query.Result.Users) return Json(200, query.Result.Users)
} }
// GET /api/search // GET /api/users/search
func SearchUsersWithPaging(c *middleware.Context) Response { func SearchUsersWithPaging(c *middleware.Context) Response {
query, err := searchUser(c) query, err := searchUser(c)
if err != nil { if err != nil {
@@ -247,6 +248,10 @@ func searchUser(c *middleware.Context) (*m.SearchUsersQuery, error) {
return nil, err return nil, err
} }
for _, user := range query.Result.Users {
user.AvatarUrl = dtos.GetGravatarUrl(user.Email)
}
query.Result.Page = page query.Result.Page = page
query.Result.PerPage = perPage query.Result.PerPage = perPage

View File

@@ -30,7 +30,7 @@ import (
_ "github.com/grafana/grafana/pkg/tsdb/testdata" _ "github.com/grafana/grafana/pkg/tsdb/testdata"
) )
var version = "4.6.0" var version = "5.0.0"
var commit = "NA" var commit = "NA"
var buildstamp string var buildstamp string
var build_date string var build_date string

View File

@@ -16,6 +16,7 @@ import (
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
@@ -26,7 +27,11 @@ type RenderOpts struct {
Height string Height string
Timeout string Timeout string
OrgId int64 OrgId int64
UserId int64
OrgRole models.RoleType
Timezone string Timezone string
IsAlertContext bool
Encoding string
} }
var ErrTimeout = errors.New("Timeout error. You can set timeout in seconds with &timeout url parameter") var ErrTimeout = errors.New("Timeout error. You can set timeout in seconds with &timeout url parameter")
@@ -74,7 +79,11 @@ func RenderToPng(params *RenderOpts) (string, error) {
pngPath, _ := filepath.Abs(filepath.Join(setting.ImagesDir, util.GetRandomString(20))) pngPath, _ := filepath.Abs(filepath.Join(setting.ImagesDir, util.GetRandomString(20)))
pngPath = pngPath + ".png" pngPath = pngPath + ".png"
renderKey := middleware.AddRenderAuthKey(params.OrgId) orgRole := params.OrgRole
if params.IsAlertContext {
orgRole = models.ROLE_ADMIN
}
renderKey := middleware.AddRenderAuthKey(params.OrgId, params.UserId, orgRole)
defer middleware.RemoveRenderAuthKey(renderKey) defer middleware.RemoveRenderAuthKey(renderKey)
timeout, err := strconv.Atoi(params.Timeout) timeout, err := strconv.Atoi(params.Timeout)
@@ -95,6 +104,10 @@ func RenderToPng(params *RenderOpts) (string, error) {
"renderKey=" + renderKey, "renderKey=" + renderKey,
} }
if params.Encoding != "" {
cmdArgs = append([]string{fmt.Sprintf("--output-encoding=%s", params.Encoding)}, cmdArgs...)
}
cmd := exec.Command(binPath, cmdArgs...) cmd := exec.Command(binPath, cmdArgs...)
stdout, err := cmd.StdoutPipe() stdout, err := cmd.StdoutPipe()

View File

@@ -87,7 +87,7 @@ func initContextWithAnonymousUser(ctx *Context) bool {
ctx.IsSignedIn = false ctx.IsSignedIn = false
ctx.AllowAnonymous = true ctx.AllowAnonymous = true
ctx.SignedInUser = &m.SignedInUser{} ctx.SignedInUser = &m.SignedInUser{IsAnonymous: true}
ctx.OrgRole = m.RoleType(setting.AnonymousOrgRole) ctx.OrgRole = m.RoleType(setting.AnonymousOrgRole)
ctx.OrgId = orgQuery.Result.Id ctx.OrgId = orgQuery.Result.Id
ctx.OrgName = orgQuery.Result.Name ctx.OrgName = orgQuery.Result.Name

View File

@@ -33,14 +33,15 @@ func initContextWithRenderAuth(ctx *Context) bool {
type renderContextFunc func(key string) (string, error) type renderContextFunc func(key string) (string, error)
func AddRenderAuthKey(orgId int64) string { func AddRenderAuthKey(orgId int64, userId int64, orgRole m.RoleType) string {
renderKeysLock.Lock() renderKeysLock.Lock()
key := util.GetRandomString(32) key := util.GetRandomString(32)
renderKeys[key] = &m.SignedInUser{ renderKeys[key] = &m.SignedInUser{
OrgId: orgId, OrgId: orgId,
OrgRole: m.ROLE_VIEWER, OrgRole: orgRole,
UserId: userId,
} }
renderKeysLock.Unlock() renderKeysLock.Unlock()

View File

@@ -0,0 +1,95 @@
package models
import (
"errors"
"time"
)
type PermissionType int
const (
PERMISSION_VIEW PermissionType = 1 << iota
PERMISSION_EDIT
PERMISSION_ADMIN
)
func (p PermissionType) String() string {
names := map[int]string{
int(PERMISSION_VIEW): "View",
int(PERMISSION_EDIT): "Edit",
int(PERMISSION_ADMIN): "Admin",
}
return names[int(p)]
}
// Typed errors
var (
ErrDashboardAclInfoMissing = errors.New("User id and team id cannot both be empty for a dashboard permission.")
ErrDashboardPermissionDashboardEmpty = errors.New("Dashboard Id must be greater than zero for a dashboard permission.")
)
// Dashboard ACL model
type DashboardAcl struct {
Id int64
OrgId int64
DashboardId int64
UserId int64
TeamId int64
Role *RoleType // pointer to be nullable
Permission PermissionType
Created time.Time
Updated time.Time
}
type DashboardAclInfoDTO struct {
Id int64 `json:"id"`
OrgId int64 `json:"-"`
DashboardId int64 `json:"dashboardId"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
UserId int64 `json:"userId"`
UserLogin string `json:"userLogin"`
UserEmail string `json:"userEmail"`
TeamId int64 `json:"teamId"`
Team string `json:"team"`
Role *RoleType `json:"role,omitempty"`
Permission PermissionType `json:"permission"`
PermissionName string `json:"permissionName"`
}
//
// COMMANDS
//
type UpdateDashboardAclCommand struct {
DashboardId int64
Items []*DashboardAcl
}
type SetDashboardAclCommand struct {
DashboardId int64
OrgId int64
UserId int64
TeamId int64
Permission PermissionType
Result DashboardAcl
}
type RemoveDashboardAclCommand struct {
AclId int64
OrgId int64
}
//
// QUERIES
//
type GetDashboardAclInfoListQuery struct {
DashboardId int64
OrgId int64
Result []*DashboardAclInfoDTO
}

View File

@@ -0,0 +1,21 @@
package models
import (
"testing"
"fmt"
. "github.com/smartystreets/goconvey/convey"
)
func TestDashboardAclModel(t *testing.T) {
Convey("When printing a PermissionType", t, func() {
view := PERMISSION_VIEW
printed := fmt.Sprint(view)
Convey("Should output a friendly name", func() {
So(printed, ShouldEqual, "View")
})
})
}

View File

@@ -16,6 +16,7 @@ var (
ErrDashboardWithSameNameExists = errors.New("A dashboard with the same name already exists") ErrDashboardWithSameNameExists = errors.New("A dashboard with the same name already exists")
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else") ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty") ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder")
ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard") ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard")
ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data") ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data")
) )
@@ -49,6 +50,9 @@ type Dashboard struct {
UpdatedBy int64 UpdatedBy int64
CreatedBy int64 CreatedBy int64
FolderId int64
IsFolder bool
HasAcl bool
Title string Title string
Data *simplejson.Json Data *simplejson.Json
@@ -66,6 +70,15 @@ func NewDashboard(title string) *Dashboard {
return dash return dash
} }
// NewDashboardFolder creates a new dashboard folder
func NewDashboardFolder(title string) *Dashboard {
folder := NewDashboard(title)
folder.Data.Set("schemaVersion", 16)
folder.Data.Set("editable", true)
folder.Data.Set("hideControls", true)
return folder
}
// GetTags turns the tags in data json into go string array // GetTags turns the tags in data json into go string array
func (dash *Dashboard) GetTags() []string { func (dash *Dashboard) GetTags() []string {
return dash.Data.Get("tags").MustStringArray() return dash.Data.Get("tags").MustStringArray()
@@ -113,6 +126,8 @@ func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard {
dash.UpdatedBy = userId dash.UpdatedBy = userId
dash.OrgId = cmd.OrgId dash.OrgId = cmd.OrgId
dash.PluginId = cmd.PluginId dash.PluginId = cmd.PluginId
dash.IsFolder = cmd.IsFolder
dash.FolderId = cmd.FolderId
dash.UpdateSlug() dash.UpdateSlug()
return dash return dash
} }
@@ -140,6 +155,8 @@ type SaveDashboardCommand struct {
OrgId int64 `json:"-"` OrgId int64 `json:"-"`
RestoredFrom int `json:"-"` RestoredFrom int `json:"-"`
PluginId string `json:"-"` PluginId string `json:"-"`
FolderId int64 `json:"folderId"`
IsFolder bool `json:"isFolder"`
UpdatedAt time.Time UpdatedAt time.Time
@@ -147,7 +164,7 @@ type SaveDashboardCommand struct {
} }
type DeleteDashboardCommand struct { type DeleteDashboardCommand struct {
Slug string Id int64
OrgId int64 OrgId int64
} }

View File

@@ -28,4 +28,27 @@ func TestDashboardModel(t *testing.T) {
}) })
}) })
Convey("Given a new dashboard folder", t, func() {
json := simplejson.New()
json.Set("title", "test dash")
cmd := &SaveDashboardCommand{Dashboard: json, IsFolder: true}
dash := cmd.GetDashboardModel()
Convey("Should set IsFolder to true", func() {
So(dash.IsFolder, ShouldBeTrue)
})
})
Convey("Given a child dashboard", t, func() {
json := simplejson.New()
json.Set("title", "test dash")
cmd := &SaveDashboardCommand{Dashboard: json, FolderId: 1}
dash := cmd.GetDashboardModel()
Convey("Should set FolderId", func() {
So(dash.FolderId, ShouldEqual, 1)
})
})
} }

View File

@@ -20,23 +20,23 @@ type RoleType string
const ( const (
ROLE_VIEWER RoleType = "Viewer" ROLE_VIEWER RoleType = "Viewer"
ROLE_EDITOR RoleType = "Editor" ROLE_EDITOR RoleType = "Editor"
ROLE_READ_ONLY_EDITOR RoleType = "Read Only Editor"
ROLE_ADMIN RoleType = "Admin" ROLE_ADMIN RoleType = "Admin"
) )
func (r RoleType) IsValid() bool { func (r RoleType) IsValid() bool {
return r == ROLE_VIEWER || r == ROLE_ADMIN || r == ROLE_EDITOR || r == ROLE_READ_ONLY_EDITOR return r == ROLE_VIEWER || r == ROLE_ADMIN || r == ROLE_EDITOR
} }
func (r RoleType) Includes(other RoleType) bool { func (r RoleType) Includes(other RoleType) bool {
if r == ROLE_ADMIN { if r == ROLE_ADMIN {
return true return true
} }
if r == ROLE_EDITOR || r == ROLE_READ_ONLY_EDITOR {
if r == ROLE_EDITOR {
return other != ROLE_ADMIN return other != ROLE_ADMIN
} }
return r == other return false
} }
func (r *RoleType) UnmarshalJSON(data []byte) error { func (r *RoleType) UnmarshalJSON(data []byte) error {
@@ -106,6 +106,7 @@ type OrgUserDTO struct {
OrgId int64 `json:"orgId"` OrgId int64 `json:"orgId"`
UserId int64 `json:"userId"` UserId int64 `json:"userId"`
Email string `json:"email"` Email string `json:"email"`
AvatarUrl string `json:"avatarUrl"`
Login string `json:"login"` Login string `json:"login"`
Role string `json:"role"` Role string `json:"role"`
LastSeenAt time.Time `json:"lastSeenAt"` LastSeenAt time.Time `json:"lastSeenAt"`

75
pkg/models/team.go Normal file
View File

@@ -0,0 +1,75 @@
package models
import (
"errors"
"time"
)
// Typed errors
var (
ErrTeamNotFound = errors.New("Team not found")
ErrTeamNameTaken = errors.New("Team name is taken")
)
// Team model
type Team struct {
Id int64 `json:"id"`
OrgId int64 `json:"orgId"`
Name string `json:"name"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
}
// ---------------------
// COMMANDS
type CreateTeamCommand struct {
Name string `json:"name" binding:"Required"`
OrgId int64 `json:"-"`
Result Team `json:"-"`
}
type UpdateTeamCommand struct {
Id int64
Name string
}
type DeleteTeamCommand struct {
Id int64
}
type GetTeamByIdQuery struct {
Id int64
Result *Team
}
type GetTeamsByUserQuery struct {
UserId int64 `json:"userId"`
Result []*Team `json:"teams"`
}
type SearchTeamsQuery struct {
Query string
Name string
Limit int
Page int
OrgId int64
Result SearchTeamQueryResult
}
type SearchTeamDto struct {
Id int64 `json:"id"`
OrgId int64 `json:"orgId"`
Name string `json:"name"`
MemberCount int64 `json:"memberCount"`
}
type SearchTeamQueryResult struct {
TotalCount int64 `json:"totalCount"`
Teams []*SearchTeamDto `json:"teams"`
Page int `json:"page"`
PerPage int `json:"perPage"`
}

55
pkg/models/team_member.go Normal file
View File

@@ -0,0 +1,55 @@
package models
import (
"errors"
"time"
)
// Typed errors
var (
ErrTeamMemberAlreadyAdded = errors.New("User is already added to this team")
)
// TeamMember model
type TeamMember struct {
Id int64
OrgId int64
TeamId int64
UserId int64
Created time.Time
Updated time.Time
}
// ---------------------
// COMMANDS
type AddTeamMemberCommand struct {
UserId int64 `json:"userId" binding:"Required"`
OrgId int64 `json:"-"`
TeamId int64 `json:"-"`
}
type RemoveTeamMemberCommand struct {
UserId int64
TeamId int64
}
// ----------------------
// QUERIES
type GetTeamMembersQuery struct {
TeamId int64
Result []*TeamMemberDTO
}
// ----------------------
// Projections and DTOs
type TeamMemberDTO struct {
OrgId int64 `json:"orgId"`
TeamId int64 `json:"teamId"`
UserId int64 `json:"userId"`
Email string `json:"email"`
Login string `json:"login"`
}

View File

@@ -160,7 +160,9 @@ type SignedInUser struct {
Name string Name string
Email string Email string
ApiKeyId int64 ApiKeyId int64
OrgCount int
IsGrafanaAdmin bool IsGrafanaAdmin bool
IsAnonymous bool
HelpFlags1 HelpFlags1 HelpFlags1 HelpFlags1
LastSeenAt time.Time LastSeenAt time.Time
} }
@@ -169,10 +171,28 @@ func (u *SignedInUser) ShouldUpdateLastSeenAt() bool {
return u.UserId > 0 && time.Since(u.LastSeenAt) > time.Minute*5 return u.UserId > 0 && time.Since(u.LastSeenAt) > time.Minute*5
} }
func (u *SignedInUser) NameOrFallback() string {
if u.Name != "" {
return u.Name
} else if u.Login != "" {
return u.Login
} else {
return u.Email
}
}
type UpdateUserLastSeenAtCommand struct { type UpdateUserLastSeenAtCommand struct {
UserId int64 UserId int64
} }
func (user *SignedInUser) HasRole(role RoleType) bool {
if user.IsGrafanaAdmin {
return true
}
return user.OrgRole.Includes(role)
}
type UserProfileDTO struct { type UserProfileDTO struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Email string `json:"email"` Email string `json:"email"`
@@ -188,6 +208,7 @@ type UserSearchHitDTO struct {
Name string `json:"name"` Name string `json:"name"`
Login string `json:"login"` Login string `json:"login"`
Email string `json:"email"` Email string `json:"email"`
AvatarUrl string `json:"avatarUrl"`
IsAdmin bool `json:"isAdmin"` IsAdmin bool `json:"isAdmin"`
LastSeenAt time.Time `json:"lastSeenAt"` LastSeenAt time.Time `json:"lastSeenAt"`
LastSeenAtAge string `json:"lastSeenAtAge"` LastSeenAtAge string `json:"lastSeenAtAge"`

View File

@@ -69,6 +69,7 @@ func ImportDashboard(cmd *ImportDashboardCommand) error {
UserId: cmd.UserId, UserId: cmd.UserId,
Overwrite: cmd.Overwrite, Overwrite: cmd.Overwrite,
PluginId: cmd.PluginId, PluginId: cmd.PluginId,
FolderId: dashboard.FolderId,
} }
if err := bus.Dispatch(&saveCmd); err != nil { if err := bus.Dispatch(&saveCmd); err != nil {

View File

@@ -13,16 +13,9 @@ import (
) )
func TestDashboardImport(t *testing.T) { func TestDashboardImport(t *testing.T) {
pluginScenario("When importing a plugin dashboard", t, func() {
Convey("When importing plugin dashboard", t, func() {
setting.Cfg = ini.Empty()
sec, _ := setting.Cfg.NewSection("plugin.test-app")
sec.NewKey("path", "../../tests/test-app")
err := Init()
So(err, ShouldBeNil)
var importedDash *m.Dashboard var importedDash *m.Dashboard
bus.AddHandler("test", func(cmd *m.SaveDashboardCommand) error { bus.AddHandler("test", func(cmd *m.SaveDashboardCommand) error {
importedDash = cmd.GetDashboardModel() importedDash = cmd.GetDashboardModel()
cmd.Result = importedDash cmd.Result = importedDash
@@ -39,7 +32,7 @@ func TestDashboardImport(t *testing.T) {
}, },
} }
err = ImportDashboard(&cmd) err := ImportDashboard(&cmd)
So(err, ShouldBeNil) So(err, ShouldBeNil)
Convey("should install dashboard", func() { Convey("should install dashboard", func() {
@@ -92,3 +85,16 @@ func TestDashboardImport(t *testing.T) {
}) })
} }
func pluginScenario(desc string, t *testing.T, fn func()) {
Convey("Given a plugin", t, func() {
setting.Cfg = ini.Empty()
sec, _ := setting.Cfg.NewSection("plugin.test-app")
sec.NewKey("path", "../../tests/test-app")
err := Init()
So(err, ShouldBeNil)
Convey(desc, fn)
})
}

View File

@@ -15,6 +15,7 @@ type PluginDashboardInfoDTO struct {
Imported bool `json:"imported"` Imported bool `json:"imported"`
ImportedUri string `json:"importedUri"` ImportedUri string `json:"importedUri"`
Slug string `json:"slug"` Slug string `json:"slug"`
DashboardId int64 `json:"dashboardId"`
ImportedRevision int64 `json:"importedRevision"` ImportedRevision int64 `json:"importedRevision"`
Revision int64 `json:"revision"` Revision int64 `json:"revision"`
Description string `json:"description"` Description string `json:"description"`
@@ -60,6 +61,7 @@ func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDT
// find existing dashboard // find existing dashboard
for _, existingDash := range query.Result { for _, existingDash := range query.Result {
if existingDash.Slug == dashboard.Slug { if existingDash.Slug == dashboard.Slug {
res.DashboardId = existingDash.Id
res.Imported = true res.Imported = true
res.ImportedUri = "db/" + existingDash.Slug res.ImportedUri = "db/" + existingDash.Slug
res.ImportedRevision = existingDash.Data.Get("revision").MustInt64(1) res.ImportedRevision = existingDash.Data.Get("revision").MustInt64(1)
@@ -75,6 +77,7 @@ func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDT
if _, exists := existingMatches[dash.Id]; !exists { if _, exists := existingMatches[dash.Id]; !exists {
result = append(result, &PluginDashboardInfoDTO{ result = append(result, &PluginDashboardInfoDTO{
Slug: dash.Slug, Slug: dash.Slug,
DashboardId: dash.Id,
Removed: true, Removed: true,
}) })
} }

View File

@@ -75,7 +75,7 @@ func syncPluginDashboards(pluginDef *PluginBase, orgId int64) {
if dash.Removed { if dash.Removed {
plog.Info("Deleting plugin dashboard", "pluginId", pluginDef.Id, "dashboard", dash.Slug) plog.Info("Deleting plugin dashboard", "pluginId", pluginDef.Id, "dashboard", dash.Slug)
deleteCmd := m.DeleteDashboardCommand{OrgId: orgId, Slug: dash.Slug} deleteCmd := m.DeleteDashboardCommand{OrgId: orgId, Id: dash.DashboardId}
if err := bus.Dispatch(&deleteCmd); err != nil { if err := bus.Dispatch(&deleteCmd); err != nil {
plog.Error("Failed to auto update app dashboard", "pluginId", pluginDef.Id, "error", err) plog.Error("Failed to auto update app dashboard", "pluginId", pluginDef.Id, "error", err)
return return
@@ -124,7 +124,7 @@ func handlePluginStateChanged(event *m.PluginStateChangedEvent) error {
return err return err
} else { } else {
for _, dash := range query.Result { for _, dash := range query.Result {
deleteCmd := m.DeleteDashboardCommand{OrgId: dash.OrgId, Slug: dash.Slug} deleteCmd := m.DeleteDashboardCommand{OrgId: dash.OrgId, Id: dash.Id}
plog.Info("Deleting plugin dashboard", "pluginId", event.PluginId, "dashboard", dash.Slug) plog.Info("Deleting plugin dashboard", "pluginId", event.PluginId, "dashboard", dash.Slug)

View File

@@ -75,14 +75,6 @@ func (c *EvalContext) ShouldUpdateAlertState() bool {
return c.Rule.State != c.PrevAlertState return c.Rule.State != c.PrevAlertState
} }
func (c *EvalContext) ShouldSendNotification() bool {
if (c.PrevAlertState == m.AlertStatePending) && (c.Rule.State == m.AlertStateOK) {
return false
}
return true
}
func (a *EvalContext) GetDurationMs() float64 { func (a *EvalContext) GetDurationMs() float64 {
return float64(a.EndTime.Nanosecond()-a.StartTime.Nanosecond()) / float64(1000000) return float64(a.EndTime.Nanosecond()-a.StartTime.Nanosecond()) / float64(1000000)
} }

View File

@@ -28,21 +28,5 @@ func TestAlertingEvalContext(t *testing.T) {
So(ctx.ShouldUpdateAlertState(), ShouldBeFalse) So(ctx.ShouldUpdateAlertState(), ShouldBeFalse)
}) })
}) })
Convey("Should send notifications", func() {
Convey("pending -> ok", func() {
ctx.PrevAlertState = models.AlertStatePending
ctx.Rule.State = models.AlertStateOK
So(ctx.ShouldSendNotification(), ShouldBeFalse)
})
Convey("ok -> alerting", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Rule.State = models.AlertStateAlerting
So(ctx.ShouldSendNotification(), ShouldBeTrue)
})
})
}) })
} }

View File

@@ -39,6 +39,11 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
break break
} }
if i == 0 {
firing = cr.Firing
noDataFound = cr.NoDataFound
}
// calculating Firing based on operator // calculating Firing based on operator
if cr.Operator == "or" { if cr.Operator == "or" {
firing = firing || cr.Firing firing = firing || cr.Firing

View File

@@ -36,6 +36,16 @@ func TestAlertingEvaluationHandler(t *testing.T) {
So(context.ConditionEvals, ShouldEqual, "true = true") So(context.ConditionEvals, ShouldEqual, "true = true")
}) })
Convey("Show return triggered with single passing condition2", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{&conditionStub{firing: true, operator: "and"}},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, true)
So(context.ConditionEvals, ShouldEqual, "true = true")
})
Convey("Show return false with not passing asdf", func() { Convey("Show return false with not passing asdf", func() {
context := NewEvalContext(context.TODO(), &Rule{ context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{ Conditions: []Condition{
@@ -131,6 +141,33 @@ func TestAlertingEvaluationHandler(t *testing.T) {
So(context.ConditionEvals, ShouldEqual, "[[true OR false] OR true] = true") So(context.ConditionEvals, ShouldEqual, "[[true OR false] OR true] = true")
}) })
Convey("Should return false if no condition is firing using OR operator", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: false, operator: "or"},
&conditionStub{firing: false, operator: "or"},
&conditionStub{firing: false, operator: "or"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, false)
So(context.ConditionEvals, ShouldEqual, "[[false OR false] OR false] = false")
})
Convey("Should retuasdfrn no data if one condition has nodata", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{operator: "or", noData: false},
&conditionStub{operator: "or", noData: false},
&conditionStub{operator: "or", noData: false},
},
})
handler.Eval(context)
So(context.NoDataFound, ShouldBeFalse)
})
Convey("Should return no data if one condition has nodata", func() { Convey("Should return no data if one condition has nodata", func() {
context := NewEvalContext(context.TODO(), &Rule{ context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{ Conditions: []Condition{

View File

@@ -15,7 +15,7 @@ type Notifier interface {
Notify(evalContext *EvalContext) error Notify(evalContext *EvalContext) error
GetType() string GetType() string
NeedsImage() bool NeedsImage() bool
PassesFilter(rule *Rule) bool ShouldNotify(evalContext *EvalContext) bool
GetNotifierId() int64 GetNotifierId() int64
GetIsDefault() bool GetIsDefault() bool

View File

@@ -24,7 +24,7 @@ type NotifierPlugin struct {
} }
type NotificationService interface { type NotificationService interface {
Send(context *EvalContext) error SendIfNeeded(context *EvalContext) error
} }
func NewNotificationService() NotificationService { func NewNotificationService() NotificationService {
@@ -41,14 +41,12 @@ func newNotificationService() *notificationService {
} }
} }
func (n *notificationService) Send(context *EvalContext) error { func (n *notificationService) SendIfNeeded(context *EvalContext) error {
notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications, context) notifiers, err := n.getNeededNotifiers(context.Rule.OrgId, context.Rule.Notifications, context)
if err != nil { if err != nil {
return err return err
} }
n.log.Info("Sending notifications for", "ruleId", context.Rule.Id, "sent count", len(notifiers))
if len(notifiers) == 0 { if len(notifiers) == 0 {
return nil return nil
} }
@@ -67,7 +65,7 @@ func (n *notificationService) sendNotifications(context *EvalContext, notifiers
for _, notifier := range notifiers { for _, notifier := range notifiers {
not := notifier //avoid updating scope variable in go routine not := notifier //avoid updating scope variable in go routine
n.log.Info("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault()) n.log.Debug("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault())
metrics.M_Alerting_Notification_Sent.WithLabelValues(not.GetType()).Inc() metrics.M_Alerting_Notification_Sent.WithLabelValues(not.GetType()).Inc()
g.Go(func() error { return not.Notify(context) }) g.Go(func() error { return not.Notify(context) })
} }
@@ -86,6 +84,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
Height: "400", Height: "400",
Timeout: "30", Timeout: "30",
OrgId: context.Rule.OrgId, OrgId: context.Rule.OrgId,
IsAlertContext: true,
} }
if slug, err := context.GetDashboardSlug(); err != nil { if slug, err := context.GetDashboardSlug(); err != nil {
@@ -109,7 +108,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
return nil return nil
} }
func (n *notificationService) getNotifiers(orgId int64, notificationIds []int64, context *EvalContext) (NotifierSlice, error) { func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []int64, context *EvalContext) (NotifierSlice, error) {
query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds} query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds}
if err := bus.Dispatch(query); err != nil { if err := bus.Dispatch(query); err != nil {
@@ -121,7 +120,7 @@ func (n *notificationService) getNotifiers(orgId int64, notificationIds []int64,
if not, err := n.createNotifierFor(notification); err != nil { if not, err := n.createNotifierFor(notification); err != nil {
return nil, err return nil, err
} else { } else {
if shouldUseNotification(not, context) { if not.ShouldNotify(context) {
result = append(result, not) result = append(result, not)
} }
} }
@@ -139,18 +138,6 @@ func (n *notificationService) createNotifierFor(model *m.AlertNotification) (Not
return notifierPlugin.Factory(model) return notifierPlugin.Factory(model)
} }
func shouldUseNotification(notifier Notifier, context *EvalContext) bool {
if !context.Firing {
return true
}
if context.Error != nil {
return true
}
return notifier.PassesFilter(context.Rule)
}
type NotifierFactory func(notification *m.AlertNotification) (Notifier, error) type NotifierFactory func(notification *m.AlertNotification) (Notifier, error)
var notifierFactories map[string]*NotifierPlugin = make(map[string]*NotifierPlugin) var notifierFactories map[string]*NotifierPlugin = make(map[string]*NotifierPlugin)

View File

@@ -1,89 +0,0 @@
package alerting
import (
"testing"
"fmt"
"github.com/grafana/grafana/pkg/models"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
type FakeNotifier struct {
FakeMatchResult bool
}
func (fn *FakeNotifier) GetType() string {
return "FakeNotifier"
}
func (fn *FakeNotifier) NeedsImage() bool {
return true
}
func (n *FakeNotifier) GetNotifierId() int64 {
return 0
}
func (n *FakeNotifier) GetIsDefault() bool {
return false
}
func (fn *FakeNotifier) Notify(alertResult *EvalContext) error { return nil }
func (fn *FakeNotifier) PassesFilter(rule *Rule) bool {
return fn.FakeMatchResult
}
func TestAlertNotificationExtraction(t *testing.T) {
Convey("Notifier tests", t, func() {
Convey("none firing alerts", func() {
ctx := &EvalContext{
Firing: false,
Rule: &Rule{
State: m.AlertStateAlerting,
},
}
notifier := &FakeNotifier{FakeMatchResult: false}
So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
})
Convey("execution error cannot be ignored", func() {
ctx := &EvalContext{
Firing: true,
Error: fmt.Errorf("I used to be a programmer just like you"),
Rule: &Rule{
State: m.AlertStateOK,
},
}
notifier := &FakeNotifier{FakeMatchResult: false}
So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
})
Convey("firing alert that match", func() {
ctx := &EvalContext{
Firing: true,
Rule: &Rule{
State: models.AlertStateAlerting,
},
}
notifier := &FakeNotifier{FakeMatchResult: true}
So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
})
Convey("firing alert that dont match", func() {
ctx := &EvalContext{
Firing: true,
Rule: &Rule{State: m.AlertStateOK},
}
notifier := &FakeNotifier{FakeMatchResult: false}
So(shouldUseNotification(notifier, ctx), ShouldBeFalse)
})
})
}

View File

@@ -0,0 +1,96 @@
package notifiers
import (
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
)
func init() {
alerting.RegisterNotifier(&alerting.NotifierPlugin{
Type: "prometheus-alertmanager",
Name: "Prometheus Alertmanager",
Description: "Sends alert to Prometheus Alertmanager",
Factory: NewAlertmanagerNotifier,
OptionsTemplate: `
<h3 class="page-heading">Alertmanager settings</h3>
<div class="gf-form">
<span class="gf-form-label width-10">Url</span>
<input type="text" required class="gf-form-input max-width-26" ng-model="ctrl.model.settings.url" placeholder="http://localhost:9093"></input>
</div>
`,
})
}
func NewAlertmanagerNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
url := model.Settings.Get("url").MustString()
if url == "" {
return nil, alerting.ValidationError{Reason: "Could not find url property in settings"}
}
return &AlertmanagerNotifier{
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
Url: url,
log: log.New("alerting.notifier.prometheus-alertmanager"),
}, nil
}
type AlertmanagerNotifier struct {
NotifierBase
Url string
log log.Logger
}
func (this *AlertmanagerNotifier) ShouldNotify(evalContext *alerting.EvalContext) bool {
return evalContext.Rule.State == m.AlertStateAlerting
}
func (this *AlertmanagerNotifier) Notify(evalContext *alerting.EvalContext) error {
alerts := make([]interface{}, 0)
for _, match := range evalContext.EvalMatches {
alertJSON := simplejson.New()
alertJSON.Set("startsAt", evalContext.StartTime.UTC().Format(time.RFC3339))
if ruleUrl, err := evalContext.GetRuleUrl(); err == nil {
alertJSON.Set("generatorURL", ruleUrl)
}
if evalContext.Rule.Message != "" {
alertJSON.SetPath([]string{"annotations", "description"}, evalContext.Rule.Message)
}
tags := make(map[string]string)
if len(match.Tags) == 0 {
tags["metric"] = match.Metric
} else {
for k, v := range match.Tags {
tags[k] = v
}
}
tags["alertname"] = evalContext.Rule.Name
alertJSON.Set("labels", tags)
alerts = append(alerts, alertJSON)
}
bodyJSON := simplejson.NewFromAny(alerts)
body, _ := bodyJSON.MarshalJSON()
cmd := &m.SendWebhookSync{
Url: this.Url + "/api/v1/alerts",
HttpMethod: "POST",
Body: string(body),
}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
this.log.Error("Failed to send alertmanager", "error", err, "alertmanager", this.Name)
return err
}
return nil
}

View File

@@ -0,0 +1,47 @@
package notifiers
import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestAlertmanagerNotifier(t *testing.T) {
Convey("Alertmanager notifier tests", t, func() {
Convey("Parsing alert notification from settings", func() {
Convey("empty settings should return error", func() {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &m.AlertNotification{
Name: "alertmanager",
Type: "alertmanager",
Settings: settingsJSON,
}
_, err := NewAlertmanagerNotifier(model)
So(err, ShouldNotBeNil)
})
Convey("from settings", func() {
json := `{ "url": "http://127.0.0.1:9093/" }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &m.AlertNotification{
Name: "alertmanager",
Type: "alertmanager",
Settings: settingsJSON,
}
not, err := NewAlertmanagerNotifier(model)
alertmanagerNotifier := not.(*AlertmanagerNotifier)
So(err, ShouldBeNil)
So(alertmanagerNotifier.Url, ShouldEqual, "http://127.0.0.1:9093/")
})
})
})
}

View File

@@ -2,6 +2,7 @@ package notifiers
import ( import (
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/alerting"
) )
@@ -14,7 +15,7 @@ type NotifierBase struct {
} }
func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model *simplejson.Json) NotifierBase { func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model *simplejson.Json) NotifierBase {
uploadImage := model.Get("uploadImage").MustBool(true) uploadImage := model.Get("uploadImage").MustBool(false)
return NotifierBase{ return NotifierBase{
Id: id, Id: id,
@@ -25,7 +26,13 @@ func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model
} }
} }
func (n *NotifierBase) PassesFilter(rule *alerting.Rule) bool { func defaultShouldNotify(context *alerting.EvalContext) bool {
if context.PrevAlertState == context.Rule.State {
return false
}
if (context.PrevAlertState == m.AlertStatePending) && (context.Rule.State == m.AlertStateOK) {
return false
}
return true return true
} }

View File

@@ -0,0 +1,32 @@
package notifiers
import (
"context"
"testing"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
. "github.com/smartystreets/goconvey/convey"
)
func TestBaseNotifier(t *testing.T) {
Convey("Base notifier tests", t, func() {
Convey("should notify", func() {
Convey("pending -> ok", func() {
context := alerting.NewEvalContext(context.TODO(), &alerting.Rule{
State: m.AlertStatePending,
})
context.Rule.State = m.AlertStateOK
So(defaultShouldNotify(context), ShouldBeFalse)
})
Convey("ok -> alerting", func() {
context := alerting.NewEvalContext(context.TODO(), &alerting.Rule{
State: m.AlertStateOK,
})
context.Rule.State = m.AlertStateAlerting
So(defaultShouldNotify(context), ShouldBeTrue)
})
})
})
}

View File

@@ -38,6 +38,10 @@ func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error)
}, nil }, nil
} }
func (this *DingDingNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
type DingDingNotifier struct { type DingDingNotifier struct {
NotifierBase NotifierBase
Url string Url string

View File

@@ -9,7 +9,7 @@ import (
) )
func TestDingDingNotifier(t *testing.T) { func TestDingDingNotifier(t *testing.T) {
Convey("Line notifier tests", t, func() { Convey("Dingding notifier tests", t, func() {
Convey("empty settings should return error", func() { Convey("empty settings should return error", func() {
json := `{ }` json := `{ }`
@@ -25,10 +25,8 @@ func TestDingDingNotifier(t *testing.T) {
}) })
Convey("settings should trigger incident", func() { Convey("settings should trigger incident", func() {
json := ` json := `{ "url": "https://www.google.com" }`
{
"url": "https://www.google.com"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json)) settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &m.AlertNotification{ model := &m.AlertNotification{
Name: "dingding_testing", Name: "dingding_testing",

View File

@@ -58,6 +58,10 @@ func NewEmailNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
}, nil }, nil
} }
func (this *EmailNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *EmailNotifier) Notify(evalContext *alerting.EvalContext) error { func (this *EmailNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Sending alert notification to", "addresses", this.Addresses) this.log.Info("Sending alert notification to", "addresses", this.Addresses)

View File

@@ -75,6 +75,10 @@ type HipChatNotifier struct {
log log.Logger log log.Logger
} }
func (this *HipChatNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error { func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Executing hipchat notification", "ruleId", evalContext.Rule.Id, "notification", this.Name) this.log.Info("Executing hipchat notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)

View File

@@ -57,6 +57,10 @@ type KafkaNotifier struct {
log log.Logger log log.Logger
} }
func (this *KafkaNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *KafkaNotifier) Notify(evalContext *alerting.EvalContext) error { func (this *KafkaNotifier) Notify(evalContext *alerting.EvalContext) error {
state := evalContext.Rule.State state := evalContext.Rule.State

View File

@@ -51,6 +51,10 @@ type LineNotifier struct {
log log.Logger log log.Logger
} }
func (this *LineNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *LineNotifier) Notify(evalContext *alerting.EvalContext) error { func (this *LineNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Executing line notification", "ruleId", evalContext.Rule.Id, "notification", this.Name) this.log.Info("Executing line notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)

View File

@@ -62,6 +62,10 @@ type OpsGenieNotifier struct {
log log.Logger log log.Logger
} }
func (this *OpsGenieNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *OpsGenieNotifier) Notify(evalContext *alerting.EvalContext) error { func (this *OpsGenieNotifier) Notify(evalContext *alerting.EvalContext) error {
var err error var err error

View File

@@ -63,6 +63,10 @@ type PagerdutyNotifier struct {
log log.Logger log log.Logger
} }
func (this *PagerdutyNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error { func (this *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error {
if evalContext.Rule.State == m.AlertStateOK && !this.AutoResolve { if evalContext.Rule.State == m.AlertStateOK && !this.AutoResolve {

View File

@@ -123,6 +123,10 @@ type PushoverNotifier struct {
log log.Logger log log.Logger
} }
func (this *PushoverNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error { func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error {
ruleUrl, err := evalContext.GetRuleUrl() ruleUrl, err := evalContext.GetRuleUrl()
if err != nil { if err != nil {

View File

@@ -71,6 +71,10 @@ type SensuNotifier struct {
log log.Logger log log.Logger
} }
func (this *SensuNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *SensuNotifier) Notify(evalContext *alerting.EvalContext) error { func (this *SensuNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Sending sensu result") this.log.Info("Sending sensu result")

View File

@@ -98,6 +98,10 @@ type SlackNotifier struct {
log log.Logger log log.Logger
} }
func (this *SlackNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error { func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Executing slack notification", "ruleId", evalContext.Rule.Id, "notification", this.Name) this.log.Info("Executing slack notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)

View File

@@ -78,7 +78,6 @@ func TestSlackNotifier(t *testing.T) {
So(slackNotifier.Mention, ShouldEqual, "@carl") So(slackNotifier.Mention, ShouldEqual, "@carl")
So(slackNotifier.Token, ShouldEqual, "xoxb-XXXXXXXX-XXXXXXXX-XXXXXXXXXX") So(slackNotifier.Token, ShouldEqual, "xoxb-XXXXXXXX-XXXXXXXX-XXXXXXXXXX")
}) })
}) })
}) })
} }

View File

@@ -47,6 +47,10 @@ type TeamsNotifier struct {
log log.Logger log log.Logger
} }
func (this *TeamsNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error { func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Executing teams notification", "ruleId", evalContext.Rule.Id, "notification", this.Name) this.log.Info("Executing teams notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)

View File

@@ -76,6 +76,10 @@ func NewTelegramNotifier(model *m.AlertNotification) (alerting.Notifier, error)
}, nil }, nil
} }
func (this *TelegramNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error { func (this *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Sending alert notification to", "bot_token", this.BotToken) this.log.Info("Sending alert notification to", "bot_token", this.BotToken)
this.log.Info("Sending alert notification to", "chat_id", this.ChatID) this.log.Info("Sending alert notification to", "chat_id", this.ChatID)

View File

@@ -114,6 +114,10 @@ func NewThreemaNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
}, nil }, nil
} }
func (this *ThreemaNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (notifier *ThreemaNotifier) Notify(evalContext *alerting.EvalContext) error { func (notifier *ThreemaNotifier) Notify(evalContext *alerting.EvalContext) error {
notifier.log.Info("Sending alert notification from", "threema_id", notifier.GatewayID) notifier.log.Info("Sending alert notification from", "threema_id", notifier.GatewayID)
notifier.log.Info("Sending alert notification to", "threema_id", notifier.RecipientID) notifier.log.Info("Sending alert notification to", "threema_id", notifier.RecipientID)

View File

@@ -68,6 +68,10 @@ type VictoropsNotifier struct {
log log.Logger log log.Logger
} }
func (this *VictoropsNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
// Notify sends notification to Victorops via POST to URL endpoint // Notify sends notification to Victorops via POST to URL endpoint
func (this *VictoropsNotifier) Notify(evalContext *alerting.EvalContext) error { func (this *VictoropsNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Executing victorops notification", "ruleId", evalContext.Rule.Id, "notification", this.Name) this.log.Info("Executing victorops notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)

View File

@@ -65,6 +65,10 @@ type WebhookNotifier struct {
log log.Logger log log.Logger
} }
func (this *WebhookNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *WebhookNotifier) Notify(evalContext *alerting.EvalContext) error { func (this *WebhookNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Sending webhook") this.log.Info("Sending webhook")

View File

@@ -18,7 +18,7 @@ func TestWebhookNotifier(t *testing.T) {
settingsJSON, _ := simplejson.NewJson([]byte(json)) settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &m.AlertNotification{ model := &m.AlertNotification{
Name: "ops", Name: "ops",
Type: "email", Type: "webhook",
Settings: settingsJSON, Settings: settingsJSON,
} }
@@ -35,7 +35,7 @@ func TestWebhookNotifier(t *testing.T) {
settingsJSON, _ := simplejson.NewJson([]byte(json)) settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &m.AlertNotification{ model := &m.AlertNotification{
Name: "ops", Name: "ops",
Type: "email", Type: "webhook",
Settings: settingsJSON, Settings: settingsJSON,
} }
@@ -44,7 +44,7 @@ func TestWebhookNotifier(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(webhookNotifier.Name, ShouldEqual, "ops") So(webhookNotifier.Name, ShouldEqual, "ops")
So(webhookNotifier.Type, ShouldEqual, "email") So(webhookNotifier.Type, ShouldEqual, "webhook")
So(webhookNotifier.Url, ShouldEqual, "http://google.com") So(webhookNotifier.Url, ShouldEqual, "http://google.com")
}) })
}) })

View File

@@ -85,11 +85,9 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
if err := annotationRepo.Save(&item); err != nil { if err := annotationRepo.Save(&item); err != nil {
handler.log.Error("Failed to save annotation for new alert state", "error", err) handler.log.Error("Failed to save annotation for new alert state", "error", err)
} }
}
if evalContext.ShouldSendNotification() { handler.notifier.SendIfNeeded(evalContext)
handler.notifier.Send(evalContext)
}
}
return nil return nil
} }

View File

@@ -23,9 +23,7 @@ func SetRepository(rep Repository) {
} }
type SaveDashboardItem struct { type SaveDashboardItem struct {
TitleLower string
OrgId int64 OrgId int64
Folder string
UpdatedAt time.Time UpdatedAt time.Time
UserId int64 UserId int64
Message string Message string
@@ -57,6 +55,8 @@ func (dr *DashboardRepository) SaveDashboard(json *SaveDashboardItem) (*models.D
OrgId: json.OrgId, OrgId: json.OrgId,
Overwrite: json.Overwrite, Overwrite: json.Overwrite,
UserId: json.UserId, UserId: json.UserId,
FolderId: dashboard.FolderId,
IsFolder: dashboard.IsFolder,
} }
if !json.UpdatedAt.IsZero() { if !json.UpdatedAt.IsZero() {

View File

@@ -0,0 +1,130 @@
package guardian
import (
"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"
)
type DashboardGuardian struct {
user *m.SignedInUser
dashId int64
orgId int64
acl []*m.DashboardAclInfoDTO
groups []*m.Team
log log.Logger
}
func NewDashboardGuardian(dashId int64, orgId int64, user *m.SignedInUser) *DashboardGuardian {
return &DashboardGuardian{
user: user,
dashId: dashId,
orgId: orgId,
log: log.New("guardians.dashboard"),
}
}
func (g *DashboardGuardian) CanSave() (bool, error) {
return g.HasPermission(m.PERMISSION_EDIT)
}
func (g *DashboardGuardian) CanEdit() (bool, error) {
if setting.ViewersCanEdit {
return g.HasPermission(m.PERMISSION_VIEW)
}
return g.HasPermission(m.PERMISSION_EDIT)
}
func (g *DashboardGuardian) CanView() (bool, error) {
return g.HasPermission(m.PERMISSION_VIEW)
}
func (g *DashboardGuardian) CanAdmin() (bool, error) {
return g.HasPermission(m.PERMISSION_ADMIN)
}
func (g *DashboardGuardian) HasPermission(permission m.PermissionType) (bool, error) {
if g.user.OrgRole == m.ROLE_ADMIN {
return true, nil
}
acl, err := g.GetAcl()
if err != nil {
return false, err
}
orgRole := g.user.OrgRole
teamAclItems := []*m.DashboardAclInfoDTO{}
for _, p := range acl {
// user match
if !g.user.IsAnonymous {
if p.UserId == g.user.UserId && p.Permission >= permission {
return true, nil
}
}
// role match
if p.Role != nil {
if *p.Role == orgRole && p.Permission >= permission {
return true, nil
}
}
// remember this rule for later
if p.TeamId > 0 {
teamAclItems = append(teamAclItems, p)
}
}
// do we have group rules?
if len(teamAclItems) == 0 {
return false, nil
}
// load groups
teams, err := g.getTeams()
if err != nil {
return false, err
}
// evalute group rules
for _, p := range acl {
for _, ug := range teams {
if ug.Id == p.TeamId && p.Permission >= permission {
return true, nil
}
}
}
return false, nil
}
// Returns dashboard acl
func (g *DashboardGuardian) GetAcl() ([]*m.DashboardAclInfoDTO, error) {
if g.acl != nil {
return g.acl, nil
}
query := m.GetDashboardAclInfoListQuery{DashboardId: g.dashId, OrgId: g.orgId}
if err := bus.Dispatch(&query); err != nil {
return nil, err
}
g.acl = query.Result
return g.acl, nil
}
func (g *DashboardGuardian) getTeams() ([]*m.Team, error) {
if g.groups != nil {
return g.groups, nil
}
query := m.GetTeamsByUserQuery{UserId: g.user.UserId}
err := bus.Dispatch(&query)
g.groups = query.Result
return query.Result, err
}

View File

@@ -121,6 +121,9 @@ func (fr *fileReader) walkFolder() error {
return nil return nil
} }
// id = 0 indicates ID validation should be avoided before writing to the db.
dash.Dashboard.Id = 0
cmd := &models.GetDashboardQuery{Slug: dash.Dashboard.Slug} cmd := &models.GetDashboardQuery{Slug: dash.Dashboard.Slug}
err = bus.Dispatch(cmd) err = bus.Dispatch(cmd)

View File

@@ -1,7 +1,6 @@
package dashboards package dashboards
import ( import (
"strings"
"time" "time"
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
@@ -23,11 +22,9 @@ func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *Das
dash := &dashboards.SaveDashboardItem{} dash := &dashboards.SaveDashboardItem{}
dash.Dashboard = models.NewDashboardFromJson(data) dash.Dashboard = models.NewDashboardFromJson(data)
dash.TitleLower = strings.ToLower(dash.Dashboard.Title)
dash.UpdatedAt = lastModified dash.UpdatedAt = lastModified
dash.Overwrite = true dash.Overwrite = true
dash.OrgId = cfg.OrgId dash.OrgId = cfg.OrgId
dash.Folder = cfg.Folder
dash.Dashboard.Data.Set("editable", cfg.Editable) dash.Dashboard.Data.Set("editable", cfg.Editable)
if dash.Dashboard.Title == "" { if dash.Dashboard.Title == "" {

View File

@@ -12,33 +12,24 @@ func Init() {
} }
func searchHandler(query *Query) error { func searchHandler(query *Query) error {
hits := make(HitList, 0)
dashQuery := FindPersistedDashboardsQuery{ dashQuery := FindPersistedDashboardsQuery{
Title: query.Title, Title: query.Title,
UserId: query.UserId, SignedInUser: query.SignedInUser,
IsStarred: query.IsStarred, IsStarred: query.IsStarred,
OrgId: query.OrgId,
DashboardIds: query.DashboardIds, DashboardIds: query.DashboardIds,
Type: query.Type,
FolderIds: query.FolderIds,
Tags: query.Tags,
Limit: query.Limit,
} }
if err := bus.Dispatch(&dashQuery); err != nil { if err := bus.Dispatch(&dashQuery); err != nil {
return err return err
} }
hits := make(HitList, 0)
hits = append(hits, dashQuery.Result...) hits = append(hits, dashQuery.Result...)
// filter out results with tag filter
if len(query.Tags) > 0 {
filtered := HitList{}
for _, hit := range hits {
if hasRequiredTags(query.Tags, hit.Tags) {
filtered = append(filtered, hit)
}
}
hits = filtered
}
// sort main result array // sort main result array
sort.Sort(hits) sort.Sort(hits)
@@ -52,7 +43,7 @@ func searchHandler(query *Query) error {
} }
// add isStarred info // add isStarred info
if err := setIsStarredFlagOnSearchResults(query.UserId, hits); err != nil { if err := setIsStarredFlagOnSearchResults(query.SignedInUser.UserId, hits); err != nil {
return err return err
} }
@@ -60,25 +51,6 @@ func searchHandler(query *Query) error {
return nil return nil
} }
func stringInSlice(a string, list []string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
func hasRequiredTags(queryTags, hitTags []string) bool {
for _, queryTag := range queryTags {
if !stringInSlice(queryTag, hitTags) {
return false
}
}
return true
}
func setIsStarredFlagOnSearchResults(userId int64, hits []*Hit) error { func setIsStarredFlagOnSearchResults(userId int64, hits []*Hit) error {
query := m.GetUserStarsQuery{UserId: userId} query := m.GetUserStarsQuery{UserId: userId}
if err := bus.Dispatch(&query); err != nil { if err := bus.Dispatch(&query); err != nil {

View File

@@ -11,13 +11,15 @@ import (
func TestSearch(t *testing.T) { func TestSearch(t *testing.T) {
Convey("Given search query", t, func() { Convey("Given search query", t, func() {
query := Query{Limit: 2000} query := Query{Limit: 2000, SignedInUser: &m.SignedInUser{IsGrafanaAdmin: true}}
bus.AddHandler("test", func(query *FindPersistedDashboardsQuery) error { bus.AddHandler("test", func(query *FindPersistedDashboardsQuery) error {
query.Result = HitList{ query.Result = HitList{
&Hit{Id: 16, Title: "CCAA", Tags: []string{"BB", "AA"}}, &Hit{Id: 16, Title: "CCAA", Type: "dash-db", Tags: []string{"BB", "AA"}},
&Hit{Id: 10, Title: "AABB", Tags: []string{"CC", "AA"}}, &Hit{Id: 10, Title: "AABB", Type: "dash-db", Tags: []string{"CC", "AA"}},
&Hit{Id: 15, Title: "BBAA", Tags: []string{"EE", "AA", "BB"}}, &Hit{Id: 15, Title: "BBAA", Type: "dash-db", Tags: []string{"EE", "AA", "BB"}},
&Hit{Id: 25, Title: "bbAAa", Type: "dash-db", Tags: []string{"EE", "AA", "BB"}},
&Hit{Id: 17, Title: "FOLDER", Type: "dash-folder"},
} }
return nil return nil
}) })
@@ -27,34 +29,29 @@ func TestSearch(t *testing.T) {
return nil return nil
}) })
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
query.Result = &m.SignedInUser{IsGrafanaAdmin: true}
return nil
})
Convey("That is empty", func() { Convey("That is empty", func() {
err := searchHandler(&query) err := searchHandler(&query)
So(err, ShouldBeNil) So(err, ShouldBeNil)
Convey("should return sorted results", func() { Convey("should return sorted results", func() {
So(query.Result[0].Title, ShouldEqual, "AABB") So(query.Result[0].Title, ShouldEqual, "FOLDER")
So(query.Result[1].Title, ShouldEqual, "BBAA") So(query.Result[1].Title, ShouldEqual, "AABB")
So(query.Result[2].Title, ShouldEqual, "CCAA") So(query.Result[2].Title, ShouldEqual, "BBAA")
So(query.Result[3].Title, ShouldEqual, "bbAAa")
So(query.Result[4].Title, ShouldEqual, "CCAA")
}) })
Convey("should return sorted tags", func() { Convey("should return sorted tags", func() {
So(query.Result[1].Tags[0], ShouldEqual, "AA") So(query.Result[3].Tags[0], ShouldEqual, "AA")
So(query.Result[1].Tags[1], ShouldEqual, "BB") So(query.Result[3].Tags[1], ShouldEqual, "BB")
So(query.Result[1].Tags[2], ShouldEqual, "EE") So(query.Result[3].Tags[2], ShouldEqual, "EE")
}) })
}) })
Convey("That filters by tag", func() {
query.Tags = []string{"BB", "AA"}
err := searchHandler(&query)
So(err, ShouldBeNil)
Convey("should return correct results", func() {
So(len(query.Result), ShouldEqual, 2)
So(query.Result[0].Title, ShouldEqual, "BBAA")
So(query.Result[1].Title, ShouldEqual, "CCAA")
})
})
}) })
} }

View File

@@ -1,37 +1,55 @@
package search package search
import "strings"
import "github.com/grafana/grafana/pkg/models"
type HitType string type HitType string
const ( const (
DashHitDB HitType = "dash-db" DashHitDB HitType = "dash-db"
DashHitHome HitType = "dash-home" DashHitHome HitType = "dash-home"
DashHitJson HitType = "dash-json" DashHitFolder HitType = "dash-folder"
DashHitScripted HitType = "dash-scripted"
) )
type Hit struct { type Hit struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Title string `json:"title"` Title string `json:"title"`
Uri string `json:"uri"` Uri string `json:"uri"`
Slug string `json:"slug"`
Type HitType `json:"type"` Type HitType `json:"type"`
Tags []string `json:"tags"` Tags []string `json:"tags"`
IsStarred bool `json:"isStarred"` IsStarred bool `json:"isStarred"`
FolderId int64 `json:"folderId,omitempty"`
FolderTitle string `json:"folderTitle,omitempty"`
FolderSlug string `json:"folderSlug,omitempty"`
} }
type HitList []*Hit type HitList []*Hit
func (s HitList) Len() int { return len(s) } func (s HitList) Len() int { return len(s) }
func (s HitList) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s HitList) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s HitList) Less(i, j int) bool { return s[i].Title < s[j].Title } func (s HitList) Less(i, j int) bool {
if s[i].Type == "dash-folder" && s[j].Type == "dash-db" {
return true
}
if s[i].Type == "dash-db" && s[j].Type == "dash-folder" {
return false
}
return strings.ToLower(s[i].Title) < strings.ToLower(s[j].Title)
}
type Query struct { type Query struct {
Title string Title string
Tags []string Tags []string
OrgId int64 OrgId int64
UserId int64 SignedInUser *models.SignedInUser
Limit int Limit int
IsStarred bool IsStarred bool
DashboardIds []int Type string
DashboardIds []int64
FolderIds []int64
Result HitList Result HitList
} }
@@ -39,9 +57,14 @@ type Query struct {
type FindPersistedDashboardsQuery struct { type FindPersistedDashboardsQuery struct {
Title string Title string
OrgId int64 OrgId int64
UserId int64 SignedInUser *models.SignedInUser
IsStarred bool IsStarred bool
DashboardIds []int DashboardIds []int64
Type string
FolderIds []int64
Tags []string
Limit int
IsBrowse bool
Result HitList Result HitList
} }

View File

@@ -12,7 +12,7 @@ func TestAlertingDataAccess(t *testing.T) {
Convey("Testing Alerting data access", t, func() { Convey("Testing Alerting data access", t, func() {
InitTestDB(t) InitTestDB(t)
testDash := insertTestDashboard("dashboard with alerts", 1, "alert") testDash := insertTestDashboard("dashboard with alerts", 1, 0, false, "alert")
items := []*m.Alert{ items := []*m.Alert{
{ {
@@ -192,7 +192,7 @@ func TestAlertingDataAccess(t *testing.T) {
err = DeleteDashboard(&m.DeleteDashboardCommand{ err = DeleteDashboard(&m.DeleteDashboardCommand{
OrgId: 1, OrgId: 1,
Slug: testDash.Slug, Id: testDash.Id,
}) })
So(err, ShouldBeNil) So(err, ShouldBeNil)

View File

@@ -1,8 +1,6 @@
package sqlstore package sqlstore
import ( import (
"bytes"
"fmt"
"time" "time"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
@@ -70,6 +68,11 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
} }
} }
err = setHasAcl(sess, dash)
if err != nil {
return err
}
parentVersion := dash.Version parentVersion := dash.Version
affectedRows := int64(0) affectedRows := int64(0)
@@ -79,14 +82,14 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
dash.Data.Set("version", dash.Version) dash.Data.Set("version", dash.Version)
affectedRows, err = sess.Insert(dash) affectedRows, err = sess.Insert(dash)
} else { } else {
dash.Version += 1 dash.Version++
dash.Data.Set("version", dash.Version) dash.Data.Set("version", dash.Version)
if !cmd.UpdatedAt.IsZero() { if !cmd.UpdatedAt.IsZero() {
dash.Updated = cmd.UpdatedAt dash.Updated = cmd.UpdatedAt
} }
affectedRows, err = sess.Id(dash.Id).Update(dash) affectedRows, err = sess.MustCols("folder_id", "has_acl").Id(dash.Id).Update(dash)
} }
if err != nil { if err != nil {
@@ -115,7 +118,7 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
return m.ErrDashboardNotFound return m.ErrDashboardNotFound
} }
// delete existing tabs // delete existing tags
_, err = sess.Exec("DELETE FROM dashboard_tag WHERE dashboard_id=?", dash.Id) _, err = sess.Exec("DELETE FROM dashboard_tag WHERE dashboard_id=?", dash.Id)
if err != nil { if err != nil {
return err return err
@@ -130,13 +133,37 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
} }
} }
} }
cmd.Result = dash cmd.Result = dash
return err return err
}) })
} }
func setHasAcl(sess *DBSession, dash *m.Dashboard) error {
// check if parent has acl
if dash.FolderId > 0 {
var parent m.Dashboard
if hasParent, err := sess.Where("folder_id=?", dash.FolderId).Get(&parent); err != nil {
return err
} else if hasParent && parent.HasAcl {
dash.HasAcl = true
}
}
// check if dash has its own acl
if dash.Id > 0 {
if res, err := sess.Query("SELECT 1 from dashboard_acl WHERE dashboard_id =?", dash.Id); err != nil {
return err
} else {
if len(res) > 0 {
dash.HasAcl = true
}
}
}
return nil
}
func GetDashboard(query *m.GetDashboardQuery) error { func GetDashboard(query *m.GetDashboardQuery) error {
dashboard := m.Dashboard{Slug: query.Slug, OrgId: query.OrgId, Id: query.Id} dashboard := m.Dashboard{Slug: query.Slug, OrgId: query.OrgId, Id: query.Id}
has, err := x.Get(&dashboard) has, err := x.Get(&dashboard)
@@ -157,60 +184,72 @@ type DashboardSearchProjection struct {
Title string Title string
Slug string Slug string
Term string Term string
IsFolder bool
FolderId int64
FolderSlug string
FolderTitle string
} }
func SearchDashboards(query *search.FindPersistedDashboardsQuery) error { func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) {
var sql bytes.Buffer limit := query.Limit
params := make([]interface{}, 0) if limit == 0 {
limit = 1000
}
sql.WriteString(`SELECT sb := NewSearchBuilder(query.SignedInUser, limit).
dashboard.id, WithTags(query.Tags).
dashboard.title, WithDashboardIdsIn(query.DashboardIds)
dashboard.slug,
dashboard_tag.term
FROM dashboard
LEFT OUTER JOIN dashboard_tag on dashboard_tag.dashboard_id = dashboard.id`)
if query.IsStarred { if query.IsStarred {
sql.WriteString(" INNER JOIN star on star.dashboard_id = dashboard.id") sb.IsStarred()
}
sql.WriteString(` WHERE dashboard.org_id=?`)
params = append(params, query.OrgId)
if query.IsStarred {
sql.WriteString(` AND star.user_id=?`)
params = append(params, query.UserId)
}
if len(query.DashboardIds) > 0 {
sql.WriteString(" AND (")
for i, dashboardId := range query.DashboardIds {
if i != 0 {
sql.WriteString(" OR")
}
sql.WriteString(" dashboard.id = ?")
params = append(params, dashboardId)
}
sql.WriteString(")")
} }
if len(query.Title) > 0 { if len(query.Title) > 0 {
sql.WriteString(" AND dashboard.title " + dialect.LikeStr() + " ?") sb.WithTitle(query.Title)
params = append(params, "%"+query.Title+"%")
} }
sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT 1000")) if len(query.Type) > 0 {
sb.WithType(query.Type)
}
if len(query.FolderIds) > 0 {
sb.WithFolderIds(query.FolderIds)
}
var res []DashboardSearchProjection var res []DashboardSearchProjection
err := x.Sql(sql.String(), params...).Find(&res) sql, params := sb.ToSql()
err := x.Sql(sql, params...).Find(&res)
if err != nil {
return nil, err
}
return res, nil
}
func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
res, err := findDashboards(query)
if err != nil { if err != nil {
return err return err
} }
makeQueryResult(query, res)
return nil
}
func getHitType(item DashboardSearchProjection) search.HitType {
var hitType search.HitType
if item.IsFolder {
hitType = search.DashHitFolder
} else {
hitType = search.DashHitDB
}
return hitType
}
func makeQueryResult(query *search.FindPersistedDashboardsQuery, res []DashboardSearchProjection) {
query.Result = make([]*search.Hit, 0) query.Result = make([]*search.Hit, 0)
hits := make(map[int64]*search.Hit) hits := make(map[int64]*search.Hit)
@@ -221,7 +260,11 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
Id: item.Id, Id: item.Id,
Title: item.Title, Title: item.Title,
Uri: "db/" + item.Slug, Uri: "db/" + item.Slug,
Type: search.DashHitDB, Slug: item.Slug,
Type: getHitType(item),
FolderId: item.FolderId,
FolderTitle: item.FolderTitle,
FolderSlug: item.FolderSlug,
Tags: []string{}, Tags: []string{},
} }
query.Result = append(query.Result, hit) query.Result = append(query.Result, hit)
@@ -231,8 +274,6 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
hit.Tags = append(hit.Tags, item.Term) hit.Tags = append(hit.Tags, item.Term)
} }
} }
return err
} }
func GetDashboardTags(query *m.GetDashboardTagsQuery) error { func GetDashboardTags(query *m.GetDashboardTagsQuery) error {
@@ -252,7 +293,7 @@ func GetDashboardTags(query *m.GetDashboardTagsQuery) error {
func DeleteDashboard(cmd *m.DeleteDashboardCommand) error { func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
return inTransaction(func(sess *DBSession) error { return inTransaction(func(sess *DBSession) error {
dashboard := m.Dashboard{Slug: cmd.Slug, OrgId: cmd.OrgId} dashboard := m.Dashboard{Id: cmd.Id, OrgId: cmd.OrgId}
has, err := sess.Get(&dashboard) has, err := sess.Get(&dashboard)
if err != nil { if err != nil {
return err return err
@@ -266,6 +307,7 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
"DELETE FROM dashboard WHERE id = ?", "DELETE FROM dashboard WHERE id = ?",
"DELETE FROM playlist_item WHERE type = 'dashboard_by_id' AND value = ?", "DELETE FROM playlist_item WHERE type = 'dashboard_by_id' AND value = ?",
"DELETE FROM dashboard_version WHERE dashboard_id = ?", "DELETE FROM dashboard_version WHERE dashboard_id = ?",
"DELETE FROM dashboard WHERE folder_id = ?",
"DELETE FROM annotation WHERE dashboard_id = ?", "DELETE FROM annotation WHERE dashboard_id = ?",
} }
@@ -304,7 +346,7 @@ func GetDashboards(query *m.GetDashboardsQuery) error {
func GetDashboardsByPluginId(query *m.GetDashboardsByPluginIdQuery) error { func GetDashboardsByPluginId(query *m.GetDashboardsByPluginIdQuery) error {
var dashboards = make([]*m.Dashboard, 0) var dashboards = make([]*m.Dashboard, 0)
err := x.Where("org_id=? AND plugin_id=?", query.OrgId, query.PluginId).Find(&dashboards) err := x.Where("org_id=? AND plugin_id=? AND is_folder=0", query.OrgId, query.PluginId).Find(&dashboards)
query.Result = dashboards query.Result = dashboards
if err != nil { if err != nil {

View File

@@ -0,0 +1,184 @@
package sqlstore
import (
"fmt"
"time"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
)
func init() {
bus.AddHandler("sql", SetDashboardAcl)
bus.AddHandler("sql", UpdateDashboardAcl)
bus.AddHandler("sql", RemoveDashboardAcl)
bus.AddHandler("sql", GetDashboardAclInfoList)
}
func UpdateDashboardAcl(cmd *m.UpdateDashboardAclCommand) error {
return inTransaction(func(sess *DBSession) error {
// delete existing items
_, err := sess.Exec("DELETE FROM dashboard_acl WHERE dashboard_id=?", cmd.DashboardId)
if err != nil {
return err
}
for _, item := range cmd.Items {
if item.UserId == 0 && item.TeamId == 0 && !item.Role.IsValid() {
return m.ErrDashboardAclInfoMissing
}
if item.DashboardId == 0 {
return m.ErrDashboardPermissionDashboardEmpty
}
sess.Nullable("user_id", "team_id")
if _, err := sess.Insert(item); err != nil {
return err
}
}
// Update dashboard HasAcl flag
dashboard := m.Dashboard{HasAcl: true}
if _, err := sess.Cols("has_acl").Where("id=? OR folder_id=?", cmd.DashboardId, cmd.DashboardId).Update(&dashboard); err != nil {
return err
}
return nil
})
}
func SetDashboardAcl(cmd *m.SetDashboardAclCommand) error {
return inTransaction(func(sess *DBSession) error {
if cmd.UserId == 0 && cmd.TeamId == 0 {
return m.ErrDashboardAclInfoMissing
}
if cmd.DashboardId == 0 {
return m.ErrDashboardPermissionDashboardEmpty
}
if res, err := sess.Query("SELECT 1 from "+dialect.Quote("dashboard_acl")+" WHERE dashboard_id =? and (team_id=? or user_id=?)", cmd.DashboardId, cmd.TeamId, cmd.UserId); err != nil {
return err
} else if len(res) == 1 {
entity := m.DashboardAcl{
Permission: cmd.Permission,
Updated: time.Now(),
}
if _, err := sess.Cols("updated", "permission").Where("dashboard_id =? and (team_id=? or user_id=?)", cmd.DashboardId, cmd.TeamId, cmd.UserId).Update(&entity); err != nil {
return err
}
return nil
}
entity := m.DashboardAcl{
OrgId: cmd.OrgId,
TeamId: cmd.TeamId,
UserId: cmd.UserId,
Created: time.Now(),
Updated: time.Now(),
DashboardId: cmd.DashboardId,
Permission: cmd.Permission,
}
cols := []string{"org_id", "created", "updated", "dashboard_id", "permission"}
if cmd.UserId != 0 {
cols = append(cols, "user_id")
}
if cmd.TeamId != 0 {
cols = append(cols, "team_id")
}
_, err := sess.Cols(cols...).Insert(&entity)
if err != nil {
return err
}
cmd.Result = entity
// Update dashboard HasAcl flag
dashboard := m.Dashboard{
HasAcl: true,
}
if _, err := sess.Cols("has_acl").Where("id=? OR folder_id=?", cmd.DashboardId, cmd.DashboardId).Update(&dashboard); err != nil {
return err
}
return nil
})
}
func RemoveDashboardAcl(cmd *m.RemoveDashboardAclCommand) error {
return inTransaction(func(sess *DBSession) error {
var rawSQL = "DELETE FROM " + dialect.Quote("dashboard_acl") + " WHERE org_id =? and id=?"
_, err := sess.Exec(rawSQL, cmd.OrgId, cmd.AclId)
if err != nil {
return err
}
return err
})
}
func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
dashboardFilter := fmt.Sprintf(`IN (
SELECT %d
UNION
SELECT folder_id from dashboard where id = %d
)`, query.DashboardId, query.DashboardId)
rawSQL := `
SELECT
da.id,
da.org_id,
da.dashboard_id,
da.user_id,
da.team_id,
da.permission,
da.role,
da.created,
da.updated,
u.login AS user_login,
u.email AS user_email,
ug.name AS team
FROM` + dialect.Quote("dashboard_acl") + ` as da
LEFT OUTER JOIN ` + dialect.Quote("user") + ` AS u ON u.id = da.user_id
LEFT OUTER JOIN team ug on ug.id = da.team_id
WHERE dashboard_id ` + dashboardFilter + ` AND da.org_id = ?
-- Also include default permission if has_acl = 0
UNION
SELECT
da.id,
da.org_id,
da.dashboard_id,
da.user_id,
da.team_id,
da.permission,
da.role,
da.created,
da.updated,
'' as user_login,
'' as user_email,
'' as team
FROM dashboard_acl as da,
dashboard as dash
LEFT JOIN dashboard folder on dash.folder_id = folder.id
WHERE dash.id = ? AND (dash.has_acl = 0 or folder.has_acl = 0) AND da.dashboard_id = -1
`
query.Result = make([]*m.DashboardAclInfoDTO, 0)
err := x.SQL(rawSQL, query.OrgId, query.DashboardId).Find(&query.Result)
for _, p := range query.Result {
p.PermissionName = p.Permission.String()
}
return err
}

View File

@@ -0,0 +1,236 @@
package sqlstore
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
m "github.com/grafana/grafana/pkg/models"
)
func TestDashboardAclDataAccess(t *testing.T) {
Convey("Testing DB", t, func() {
InitTestDB(t)
Convey("Given a dashboard folder and a user", func() {
currentUser := createUser("viewer", "Viewer", false)
savedFolder := insertTestDashboard("1 test dash folder", 1, 0, true, "prod", "webapp")
childDash := insertTestDashboard("2 test dash", 1, savedFolder.Id, false, "prod", "webapp")
Convey("When adding dashboard permission with userId and teamId set to 0", func() {
err := SetDashboardAcl(&m.SetDashboardAclCommand{
OrgId: 1,
DashboardId: savedFolder.Id,
Permission: m.PERMISSION_EDIT,
})
So(err, ShouldEqual, m.ErrDashboardAclInfoMissing)
})
Convey("Given dashboard folder with default permissions", func() {
Convey("When reading dashboard acl should include acl for parent folder", func() {
query := m.GetDashboardAclInfoListQuery{DashboardId: childDash.Id, OrgId: 1}
err := GetDashboardAclInfoList(&query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 2)
defaultPermissionsId := -1
So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[0].Role, ShouldEqual, m.ROLE_VIEWER)
So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[1].Role, ShouldEqual, m.ROLE_EDITOR)
})
})
Convey("Given dashboard folder permission", func() {
err := SetDashboardAcl(&m.SetDashboardAclCommand{
OrgId: 1,
UserId: currentUser.Id,
DashboardId: savedFolder.Id,
Permission: m.PERMISSION_EDIT,
})
So(err, ShouldBeNil)
Convey("When reading dashboard acl should include acl for parent folder", func() {
query := m.GetDashboardAclInfoListQuery{DashboardId: childDash.Id, OrgId: 1}
err := GetDashboardAclInfoList(&query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 1)
So(query.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
})
Convey("Given child dashboard permission", func() {
err := SetDashboardAcl(&m.SetDashboardAclCommand{
OrgId: 1,
UserId: currentUser.Id,
DashboardId: childDash.Id,
Permission: m.PERMISSION_EDIT,
})
So(err, ShouldBeNil)
Convey("When reading dashboard acl should include acl for parent folder and child", func() {
query := m.GetDashboardAclInfoListQuery{OrgId: 1, DashboardId: childDash.Id}
err := GetDashboardAclInfoList(&query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 2)
So(query.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
So(query.Result[1].DashboardId, ShouldEqual, childDash.Id)
})
})
})
Convey("Given child dashboard permission in folder with no permissions", func() {
err := SetDashboardAcl(&m.SetDashboardAclCommand{
OrgId: 1,
UserId: currentUser.Id,
DashboardId: childDash.Id,
Permission: m.PERMISSION_EDIT,
})
So(err, ShouldBeNil)
Convey("When reading dashboard acl should include default acl for parent folder and the child acl", func() {
query := m.GetDashboardAclInfoListQuery{OrgId: 1, DashboardId: childDash.Id}
err := GetDashboardAclInfoList(&query)
So(err, ShouldBeNil)
defaultPermissionsId := -1
So(len(query.Result), ShouldEqual, 3)
So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[0].Role, ShouldEqual, m.ROLE_VIEWER)
So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[1].Role, ShouldEqual, m.ROLE_EDITOR)
So(query.Result[2].DashboardId, ShouldEqual, childDash.Id)
})
})
Convey("Should be able to add dashboard permission", func() {
setDashAclCmd := m.SetDashboardAclCommand{
OrgId: 1,
UserId: currentUser.Id,
DashboardId: savedFolder.Id,
Permission: m.PERMISSION_EDIT,
}
err := SetDashboardAcl(&setDashAclCmd)
So(err, ShouldBeNil)
So(setDashAclCmd.Result.Id, ShouldEqual, 3)
q1 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
err = GetDashboardAclInfoList(q1)
So(err, ShouldBeNil)
So(q1.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
So(q1.Result[0].Permission, ShouldEqual, m.PERMISSION_EDIT)
So(q1.Result[0].PermissionName, ShouldEqual, "Edit")
So(q1.Result[0].UserId, ShouldEqual, currentUser.Id)
So(q1.Result[0].UserLogin, ShouldEqual, currentUser.Login)
So(q1.Result[0].UserEmail, ShouldEqual, currentUser.Email)
So(q1.Result[0].Id, ShouldEqual, setDashAclCmd.Result.Id)
Convey("Should update hasAcl field to true for dashboard folder and its children", func() {
q2 := &m.GetDashboardsQuery{DashboardIds: []int64{savedFolder.Id, childDash.Id}}
err := GetDashboards(q2)
So(err, ShouldBeNil)
So(q2.Result[0].HasAcl, ShouldBeTrue)
So(q2.Result[1].HasAcl, ShouldBeTrue)
})
Convey("Should be able to update an existing permission", func() {
err := SetDashboardAcl(&m.SetDashboardAclCommand{
OrgId: 1,
UserId: 1,
DashboardId: savedFolder.Id,
Permission: m.PERMISSION_ADMIN,
})
So(err, ShouldBeNil)
q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
err = GetDashboardAclInfoList(q3)
So(err, ShouldBeNil)
So(len(q3.Result), ShouldEqual, 1)
So(q3.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
So(q3.Result[0].Permission, ShouldEqual, m.PERMISSION_ADMIN)
So(q3.Result[0].UserId, ShouldEqual, 1)
})
Convey("Should be able to delete an existing permission", func() {
err := RemoveDashboardAcl(&m.RemoveDashboardAclCommand{
OrgId: 1,
AclId: setDashAclCmd.Result.Id,
})
So(err, ShouldBeNil)
q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
err = GetDashboardAclInfoList(q3)
So(err, ShouldBeNil)
So(len(q3.Result), ShouldEqual, 0)
})
})
Convey("Given a team", func() {
group1 := m.CreateTeamCommand{Name: "group1 name", OrgId: 1}
err := CreateTeam(&group1)
So(err, ShouldBeNil)
Convey("Should be able to add a user permission for a team", func() {
setDashAclCmd := m.SetDashboardAclCommand{
OrgId: 1,
TeamId: group1.Result.Id,
DashboardId: savedFolder.Id,
Permission: m.PERMISSION_EDIT,
}
err := SetDashboardAcl(&setDashAclCmd)
So(err, ShouldBeNil)
q1 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
err = GetDashboardAclInfoList(q1)
So(err, ShouldBeNil)
So(q1.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
So(q1.Result[0].Permission, ShouldEqual, m.PERMISSION_EDIT)
So(q1.Result[0].TeamId, ShouldEqual, group1.Result.Id)
Convey("Should be able to delete an existing permission for a team", func() {
err := RemoveDashboardAcl(&m.RemoveDashboardAclCommand{
OrgId: 1,
AclId: setDashAclCmd.Result.Id,
})
So(err, ShouldBeNil)
q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
err = GetDashboardAclInfoList(q3)
So(err, ShouldBeNil)
So(len(q3.Result), ShouldEqual, 0)
})
})
Convey("Should be able to update an existing permission for a team", func() {
err := SetDashboardAcl(&m.SetDashboardAclCommand{
OrgId: 1,
TeamId: group1.Result.Id,
DashboardId: savedFolder.Id,
Permission: m.PERMISSION_ADMIN,
})
So(err, ShouldBeNil)
q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
err = GetDashboardAclInfoList(q3)
So(err, ShouldBeNil)
So(len(q3.Result), ShouldEqual, 1)
So(q3.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
So(q3.Result[0].Permission, ShouldEqual, m.PERMISSION_ADMIN)
So(q3.Result[0].TeamId, ShouldEqual, group1.Result.Id)
})
})
})
})
}

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