Compare commits

..

1 Commits

Author SHA1 Message Date
Grot (@grafanabot)
c256012971 "Release: Updated versions in package to 9.0.4" (#429) 2022-07-20 15:23:57 +01:00
162 changed files with 1112 additions and 3836 deletions

View File

@@ -3681,7 +3681,8 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
],
"public/app/core/utils/dag.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/core/utils/deferred.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
@@ -4543,6 +4544,12 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/features/dashboard/components/SubMenu/DashboardLinksDashboard.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
],
"public/app/features/dashboard/components/SubMenu/SubMenu.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
@@ -4843,8 +4850,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "24"],
[0, 0, 0, "Do not use any type assertions.", "25"],
[0, 0, 0, "Unexpected any. Specify a different type.", "26"],
[0, 0, 0, "Unexpected any. Specify a different type.", "27"],
[0, 0, 0, "Unexpected any. Specify a different type.", "28"]
[0, 0, 0, "Unexpected any. Specify a different type.", "27"]
],
"public/app/features/dashboard/state/TimeModel.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
@@ -6224,6 +6230,9 @@ exports[`better eslint`] = {
"public/app/features/transformers/utils.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/users/UsersActionBar.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/users/__mocks__/userMocks.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
@@ -7866,7 +7875,8 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "23"],
[0, 0, 0, "Unexpected any. Specify a different type.", "24"],
[0, 0, 0, "Unexpected any. Specify a different type.", "25"],
[0, 0, 0, "Unexpected any. Specify a different type.", "26"]
[0, 0, 0, "Unexpected any. Specify a different type.", "26"],
[0, 0, 0, "Unexpected any. Specify a different type.", "27"]
],
"public/app/plugins/datasource/loki/datasource.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
@@ -9031,6 +9041,12 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
],
"public/app/plugins/panel/alertlist/UnifiedAlertList.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/plugins/panel/alertlist/unified-alerting/UngroupedView.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/plugins/panel/annolist/AnnoListPanel.test.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],

1
.bingo/.gitignore vendored
View File

@@ -5,7 +5,6 @@
# But not these files:
!.gitignore
!*.mod
!*.sum
!README.md
!Variables.mk
!variables.env

View File

@@ -1,4 +1,4 @@
# Auto generated binary variables helper managed by https://github.com/bwplotka/bingo v0.6. DO NOT EDIT.
# Auto generated binary variables helper managed by https://github.com/bwplotka/bingo v0.5.1. DO NOT EDIT.
# All tools are designed to be build inside $GOBIN.
BINGO_DIR := $(dir $(lastword $(MAKEFILE_LIST)))
GOPATH ?= $(shell go env GOPATH)
@@ -17,11 +17,11 @@ GO ?= $(shell which go)
# @echo "Running drone"
# @$(DRONE) <flags/args..>
#
DRONE := $(GOBIN)/drone-v1.5.0
DRONE := $(GOBIN)/drone-v1.4.0
$(DRONE): $(BINGO_DIR)/drone.mod
@# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies.
@echo "(re)installing $(GOBIN)/drone-v1.5.0"
@cd $(BINGO_DIR) && $(GO) build -mod=mod -modfile=drone.mod -o=$(GOBIN)/drone-v1.5.0 "github.com/drone/drone-cli/drone"
@echo "(re)installing $(GOBIN)/drone-v1.4.0"
@cd $(BINGO_DIR) && $(GO) build -mod=mod -modfile=drone.mod -o=$(GOBIN)/drone-v1.4.0 "github.com/drone/drone-cli/drone"
WIRE := $(GOBIN)/wire-v0.5.0
$(WIRE): $(BINGO_DIR)/wire.mod

View File

@@ -4,4 +4,4 @@ go 1.17
replace github.com/docker/docker => github.com/docker/engine v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible
require github.com/drone/drone-cli v1.5.0 // drone
require github.com/drone/drone-cli v1.4.0 // drone

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
# Auto generated binary variables helper managed by https://github.com/bwplotka/bingo v0.6. DO NOT EDIT.
# Auto generated binary variables helper managed by https://github.com/bwplotka/bingo v0.5.1. DO NOT EDIT.
# All tools are designed to be build inside $GOBIN.
# Those variables will work only until 'bingo get' was invoked, or if tools were installed via Makefile's Variables.mk.
GOBIN=${GOBIN:=$(go env GOBIN)}
@@ -8,7 +8,7 @@ if [ -z "$GOBIN" ]; then
fi
DRONE="${GOBIN}/drone-v1.5.0"
DRONE="${GOBIN}/drone-v1.4.0"
WIRE="${GOBIN}/wire-v0.5.0"

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,4 @@ on:
jobs:
workflow-call:
uses: grafana/code-coverage/.github/workflows/code-coverage.yml@v0.1.6
with:
frontend-path-regexp: public\/app\/plugins\/datasource\/(grafana-azure-monitor-datasource|cloud-monitoring|cloudwatch)
backend-path-regexp: pkg\/tsdb\/(azuremonitor|cloudmonitoring|cloudwatch)
uses: grafana/code-coverage/.github/workflows/code-coverage.yml@v0.1.2

View File

@@ -1,69 +1,3 @@
<!-- 9.0.6 START -->
# 9.0.6 (2022-08-01)
### Features and enhancements
- **Access Control:** Allow org admins to invite new users to their organization. [#52904](https://github.com/grafana/grafana/pull/52904), [@IevaVasiljeva](https://github.com/IevaVasiljeva)
### Bug fixes
- **Grafana/toolkit:** Fix incorrect image and font generation for plugin builds. [#52927](https://github.com/grafana/grafana/pull/52927), [@academo](https://github.com/academo)
- **Prometheus:** Fix adding of multiple values for regex operator. [#52978](https://github.com/grafana/grafana/pull/52978), [@ivanahuckova](https://github.com/ivanahuckova)
- **UI/Card:** Fix card items always having pointer cursor. [#52809](https://github.com/grafana/grafana/pull/52809), [@gillesdemey](https://github.com/gillesdemey)
<!-- 9.0.6 END -->
<!-- 9.0.5 START -->
# 9.0.5 (2022-07-26)
### Features and enhancements
- **Access control:** Show dashboard settings to users who can edit dashboard. [#52535](https://github.com/grafana/grafana/pull/52535), [@grafanabot](https://github.com/grafanabot)
- **Alerting:** Allow the webhook notifier to support a custom Authorization header. [#52515](https://github.com/grafana/grafana/pull/52515), [@gotjosh](https://github.com/gotjosh)
- **Chore:** Upgrade to Go version 1.17.12. [#52523](https://github.com/grafana/grafana/pull/52523), [@sakjur](https://github.com/sakjur)
- **Plugins:** Add signature wildcard globbing for dedicated private plugin type. [#52163](https://github.com/grafana/grafana/pull/52163), [@wbrowne](https://github.com/wbrowne)
- **Prometheus:** Don't show errors from unsuccessful API checks like rules or exemplar checks. [#52193](https://github.com/grafana/grafana/pull/52193), [@darrenjaneczek](https://github.com/darrenjaneczek)
### Bug fixes
- **Access control:** Allow organisation admins to add existing users to org (#51668). [#52553](https://github.com/grafana/grafana/pull/52553), [@vtorosyan](https://github.com/vtorosyan)
- **Alerting:** Fix alert panel instance-based rules filtering. [#52583](https://github.com/grafana/grafana/pull/52583), [@konrad147](https://github.com/konrad147)
- **Apps:** Fixes navigation between different app plugin pages. [#52571](https://github.com/grafana/grafana/pull/52571), [@torkelo](https://github.com/torkelo)
- **Cloudwatch:** Upgrade grafana-aws-sdk to fix auth issue with secret keys. [#52420](https://github.com/grafana/grafana/pull/52420), [@sarahzinger](https://github.com/sarahzinger)
- **Grafana/toolkit:** Fix incorrect image and font generation for plugin builds. [#52661](https://github.com/grafana/grafana/pull/52661), [@academo](https://github.com/academo)
- **Loki:** Fix `show context` not working in some occasions. [#52458](https://github.com/grafana/grafana/pull/52458), [@svennergr](https://github.com/svennergr)
- **RBAC:** Fix permissions on dashboards and folders created by anonymous users. [#52615](https://github.com/grafana/grafana/pull/52615), [@gamab](https://github.com/gamab)
<!-- 9.0.5 END -->
<!-- 9.0.4 START -->
# 9.0.4 (2022-07-20)
### Features and enhancements
- **Browse/Search:** Make browser back work properly when visiting Browse or search. [#52271](https://github.com/grafana/grafana/pull/52271), [@torkelo](https://github.com/torkelo)
- **Logs:** Improve getLogRowContext API. [#52130](https://github.com/grafana/grafana/pull/52130), [@gabor](https://github.com/gabor)
- **Loki:** Improve handling of empty responses. [#52397](https://github.com/grafana/grafana/pull/52397), [@gabor](https://github.com/gabor)
- **Plugins:** Always validate root URL if specified in signature manfiest. [#52332](https://github.com/grafana/grafana/pull/52332), [@wbrowne](https://github.com/wbrowne)
- **Preferences:** Get home dashboard from teams. [#52225](https://github.com/grafana/grafana/pull/52225), [@sakjur](https://github.com/sakjur)
- **SQLStore:** Support Upserting multiple rows. [#52228](https://github.com/grafana/grafana/pull/52228), [@joeblubaugh](https://github.com/joeblubaugh)
- **Traces:** Add more template variables in Tempo & Zipkin. [#52306](https://github.com/grafana/grafana/pull/52306), [@joey-grafana](https://github.com/joey-grafana)
- **Traces:** Remove serviceMap feature flag. [#52375](https://github.com/grafana/grafana/pull/52375), [@joey-grafana](https://github.com/joey-grafana)
### Bug fixes
- **Access Control:** Fix missing folder permissions. [#52410](https://github.com/grafana/grafana/pull/52410), [@IevaVasiljeva](https://github.com/IevaVasiljeva)
- **Access control:** Fix org user removal for OSS users. [#52473](https://github.com/grafana/grafana/pull/52473), [@IevaVasiljeva](https://github.com/IevaVasiljeva)
- **Alerting:** Fix Slack notification preview. [#50230](https://github.com/grafana/grafana/pull/50230), [@ekrucio](https://github.com/ekrucio)
- **Alerting:** Fix Slack push notifications. [#52391](https://github.com/grafana/grafana/pull/52391), [@grobinson-grafana](https://github.com/grobinson-grafana)
- **Alerting:** Fixes slack push notifications. [#50267](https://github.com/grafana/grafana/pull/50267), [@jgillick](https://github.com/jgillick)
- **Alerting:** Preserve new-lines from custom email templates in rendered email. [#52253](https://github.com/grafana/grafana/pull/52253), [@alexweav](https://github.com/alexweav)
- **Insights:** Fix dashboard and data source insights pages. (Enterprise)
- **Log:** Fix text logging for unsupported types. [#51306](https://github.com/grafana/grafana/pull/51306), [@papagian](https://github.com/papagian)
- **Loki:** Fix incorrect TopK value type in query builder. [#52226](https://github.com/grafana/grafana/pull/52226), [@ivanahuckova](https://github.com/ivanahuckova)
<!-- 9.0.4 END -->
<!-- 9.0.3 START -->
# 9.0.3 (2022-07-14)

View File

@@ -20,13 +20,14 @@ COPY emails emails
ENV NODE_ENV production
RUN yarn build
FROM golang:1.17.12-alpine3.15 as go-builder
FROM golang:1.17.11-alpine3.15 as go-builder
RUN apk add --no-cache gcc g++ make
WORKDIR /grafana
COPY go.mod go.sum embed.go Makefile build.go package.json ./
COPY cue cue
COPY packages/grafana-schema packages/grafana-schema
COPY public/app/plugins public/app/plugins
COPY public/api-spec.json public/api-spec.json

View File

@@ -21,7 +21,7 @@ COPY emails emails
ENV NODE_ENV production
RUN yarn build
FROM golang:1.17.12 AS go-builder
FROM golang:1.17.11 AS go-builder
WORKDIR /src/grafana
@@ -29,6 +29,7 @@ COPY go.mod go.sum embed.go ./
COPY Makefile build.go package.json ./
COPY .bingo .bingo
COPY pkg pkg/
COPY cue cue/
COPY cue.mod cue.mod/
COPY packages/grafana-schema packages/grafana-schema/
COPY public/app/plugins public/app/plugins/

View File

@@ -125,7 +125,7 @@ path = grafana.db
# For "sqlite3" only. cache mode setting used for connecting to the database
cache_mode = private
# For "mysql" only if migrationLocking feature toggle is set. How many seconds to wait before failing to lock the database for the migrations, default is 0.
# For "mysql" only if lockingMigration feature toggle is set. How many seconds to wait before failing to lock the database for the migrations, default is 0.
locking_attempt_timeout_sec = 0
#################################### Cache server #############################
@@ -842,8 +842,8 @@ max_attempts = 3
min_interval = 10s
[unified_alerting.screenshots]
# Enable screenshots in notifications. This option requires the Grafana Image Renderer plugin.
# For more information on configuration options, refer to [rendering].
# Enable screenshots in notifications. This option requires a remote HTTP image rendering service. Please
# see [rendering] for further configuration options.
capture = false
# The maximum number of screenshots that can be taken at the same time. This option is different from

View File

@@ -126,7 +126,7 @@
# For "sqlite3" only. cache mode setting used for connecting to the database. (private, shared)
;cache_mode = private
# For "mysql" only if migrationLocking feature toggle is set. How many seconds to wait before failing to lock the database for the migrations, default is 0.
# For "mysql" only if lockingMigration feature toggle is set. How many seconds to wait before failing to lock the database for the migrations, default is 0.
;locking_attempt_timeout_sec = 0
################################### Data sources #########################

View File

@@ -191,4 +191,4 @@ import { Plural } from "@lingui/macro"
## Documentation
[Grafana's documentation](https://grafana.com/docs/grafana/latest/) is not yet open for translation and should be authored in American English only.
[Grafana's documentation](https://grafana.com/docs/grafana/latest/) is not yet open for translation and should be authored in English only.

View File

@@ -14,18 +14,24 @@ weight: 700
# API keys
An API key is a randomly generated string that external systems use to interact with Grafana HTTP APIs.
API keys can be used to interact with Grafana HTTP APIs.
When you create an API key, you specify a **Role** that determines the permissions associated with the API key. Role permissions control that actions the API key can perform on Grafana resources.
> **Note:** If you use Grafana v8.5 or newer, use service accounts instead of API keys. For more information, refer to [Service accounts]({{< relref "../service-accounts/" >}}).
We recommend using service accounts instead of API keys if you are on Grafana 8.5+, for more information refer to [About service accounts]({{< relref "../service-accounts/about-service-accounts/#" >}}).
{{< section >}}
## About API keys
An API key is a randomly generated string that external systems use to interact with Grafana HTTP APIs.
When you create an API key, you specify a **Role** that determines the permissions associated with the API key. Role permissions control that actions the API key can perform on Grafana resources. For more information about creating API keys, refer to [Create an API key]({{< relref "create-api-key/#" >}}).
## Create an API key
Create an API key when you want to manage your computed workload with a user.
For more information about API keys, refer to [About API keys in Grafana]({{< relref "about-api-keys/" >}}).
This topic shows you how to create an API key using the Grafana UI. You can also create an API key using the Grafana HTTP API. For more information about creating API keys via the API, refer to [Create API key via API]({{< relref "../../developers/http_api/create-api-tokens-for-org/#how-to-create-a-new-organization-and-an-api-token" >}}).
### Before you begin:

View File

@@ -11,7 +11,7 @@ weight: 300
# Roles and permissions
A _user_ is any individual who can log in to Grafana. Each user is associated with a _role_ that includes _permissions_. Permissions determine the tasks a user can perform in the system. For example, the **Admin** role includes permissions for an administrator to create and delete users.
A _user_ is defined as any individual who can log in to Grafana. Each user is associated with a _role_ that includes _permissions_. Permissions determine the tasks a user can perform in the system. For example, the **Admin** role includes permissions for an administrator to create and delete users.
You can assign a user one of three types of permissions:

View File

@@ -77,7 +77,7 @@ The following list contains role-based access control actions.
| `licensing:read` | n/a | Read licensing information. |
| `licensing:write` | n/a | Update the license token. |
| `org.users:write` | `users:*` <br> `users:id:*` | Update the organization role (`Viewer`, `Editor`, or `Admin`) of a user. |
| `org.users:add` | `users:*` | Add a user to an organization or invite a new user to an organization. |
| `org.users:add` | `users:*` | Add a user to an organization. |
| `org.users:read` | `users:*` <br> `users:id:*` | Get user profiles within an organization. |
| `org.users:remove` | `users:*` <br> `users:id:*` | Remove a user from an organization. |
| `org:create` | n/a | Create an organization. |

View File

@@ -60,7 +60,7 @@ The following tables list permissions associated with basic and fixed roles.
| `fixed:licensing:reader` | `licensing:read`<br>`licensing.reports:read` | Read licensing information and licensing reports. |
| `fixed:licensing:writer` | All permissions from `fixed:licensing:viewer` and <br>`licensing:write`<br>`licensing:delete` | Read licensing information and licensing reports, update and delete the license token. |
| `fixed:org.users:reader` | `org.users:read` | Read users within a single organization. |
| `fixed:org.users:writer` | All permissions from `fixed:org.users:reader` and <br>`org.users:add`<br>`org.users:remove`<br>`org.users:write` | Within a single organization, add a user, invite a new user, read information about a user and their role, remove a user from that organization, or change the role of a user. |
| `fixed:org.users:writer` | All permissions from `fixed:org.users:reader` and <br>`org.users:add`<br>`org.users:remove`<br>`org.users:write` | Within a single organization, add a user, invite a user, read information about a user and their role, remove a user from that organization, or change the role of a user. |
| `fixed:organization:maintainer` | All permissions from `fixed:organization:reader` and <br> `orgs:write`<br>`orgs:create`<br>`orgs:delete`<br>`orgs.quotas:write` | Create, read, write, or delete an organization. Read or write its quotas. This role needs to be assigned globally. |
| `fixed:organization:reader` | `orgs:read`<br>`orgs.quotas:read` | Read an organization and its quotas. |
| `fixed:organization:writer` | All permissions from `fixed:organization:reader` and <br> `orgs:write`<br>`orgs.preferences:read`<br>`orgs.preferences:write` | Read an organization, its quotas, or its preferences. Update organization properties, or its preferences. |

View File

@@ -16,7 +16,13 @@ weight: 800
# Service accounts
You can use a service account to run automated workloads in Grafana, such as dashboard provisioning, configuration, or report generation. Create service accounts and tokens to authenticate applications, such as Terraform, with the Grafana API.
You can use service accounts to run automated or compute workloads.
{{< section >}}
## About service accounts
A service account can be used to run automated workloads in Grafana, like dashboard provisioning, configuration, or report generation. Create service accounts and tokens to authenticate applications like Terraform with the Grafana API.
> **Note:** Service accounts are available in Grafana 8.5+ as a beta feature. To enable service accounts, refer to [Enable service accounts]({{< relref "enable-service-accounts/#" >}}) section. Service accounts will eventually replace [API keys]({{< relref "../api-keys/" >}}) as the primary way to authenticate applications that interact with Grafana.

View File

@@ -11,7 +11,7 @@ weight: 114
Grafana Alerting allows you to learn about problems in your systems moments after they occur. Create, manage, and take action on your alerts in a single, consolidated view, and improve your teams ability to identify and resolve issues quickly.
Grafana Alerting is available for Grafana OSS, Grafana Enterprise, or Grafana Cloud. With Mimir and Loki alert rules you can run alert expressions closer to your data and at massive scale, all managed by the Grafana UI you are already familiar with.
Grafana Alerting is available for for Grafana OSS, Grafana Enterprise, or Grafana Cloud. With Mimir and Loki alert rules you can run alert expressions closer to your data and at massive scale, all managed by the Grafana UI you are already familiar with.
Watch this video to learn more about Grafana Alerting: {{< vimeo 720001629 >}}

View File

@@ -15,7 +15,7 @@ weight: 400
# Create a Grafana managed alerting rule
Grafana allows you to create alerting rules that query one or more data sources, reduce or transform the results and compare them to each other or to fix thresholds. When these are executed, Grafana sends notifications to the contact point. For information on Grafana Alerting, see [About Grafana Alerting]({{< relref "../" >}}) which explains the various components of Grafana Alerting. We also recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "../fundamentals/" >}}) of Grafana Alerting.
Grafana allows you to create alerting rules that query one or more data sources, reduce or transform the results and compare them to each other or to fix thresholds. When these are executed, Grafana sends notifications to the contact point. For information on Grafana Alerting, see [About Grafana Alerting]({{< relref "../about-alerting/" >}}) which explains the various components of Grafana Alerting. We also recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "../fundamentals/" >}}) of Grafana Alerting.
Watch this video to learn more about creating alerts: {{< vimeo 720001934 >}}

View File

@@ -27,7 +27,7 @@ You can create and manage recording rules for an external Grafana Mimir or Loki
- **Loki** - The `local` rule storage type, default for the Loki data source, supports only viewing of rules. To edit rules, configure one of the other rule storage types.
- **Grafana Mimir** - use the `/prometheus` prefix. The Prometheus data source supports both Grafana Mimir and Prometheus, and Grafana expects that both the [Query API](https://grafana.com/docs/mimir/latest/operators-guide/reference-http-api/#querier--query-frontend) and [Ruler API](https://grafana.com/docs/mimir/latest/operators-guide/reference-http-api/#ruler) are under the same URL. You cannot provide a separate URL for the Ruler API.
- **Grafana Mimir** - use the [legacy `/api/prom` prefix](https://grafana.com/docs/mimir/latest/operators-guide/reference-http-api/#path-prefixes), not `/prometheus`. The Prometheus data source supports both Grafana Mimir and Prometheus, and Grafana expects that both the [Query API](https://grafana.com/docs/mimir/latest/operators-guide/reference-http-api/#querier--query-frontend) and [Ruler API](https://grafana.com/docs/mimir/latest/operators-guide/reference-http-api/#ruler) are under the same URL. You cannot provide a separate URL for the Ruler API.
> **Note:** If you do not want to manage alerting rules for a particular Loki or Prometheus data source, go to its settings and clear the **Manage alerts via Alerting UI** checkbox.

View File

@@ -17,7 +17,7 @@ weight: 400
# Create a Grafana Mimir or Loki managed alerting rule
Grafana allows you to create alerting rules for an external Grafana Mimir or Loki instance that has ruler API enabled. For information on Grafana Alerting, see [About Grafana Alerting]({{< relref "../" >}}) which explains the various components of Grafana Alerting. We also recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "../fundamentals/" >}}) of Grafana Alerting.
Grafana allows you to create alerting rules for an external Grafana Mimir or Loki instance that has ruler API enabled. For information on Grafana Alerting, see [About Grafana Alerting]({{< relref "../about-alerting/" >}}) which explains the various components of Grafana Alerting. We also recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "../fundamentals/" >}}) of Grafana Alerting.
## Before you begin

View File

@@ -10,13 +10,13 @@ keywords:
- guide
- contact point
- templating
title: List of contact point types
title: List of notifiers
weight: 130
---
# List of supported contact point types
# List of supported notifiers
The following table lists the contact point types supported by Grafana.
The following table lists the notifiers (contact point types) supported by Grafana.
| Name | Type | Grafana Alertmanager | Other Alertmanagers |
| ------------------------------------------------ | ------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------- |

View File

@@ -23,7 +23,7 @@ These are the data sources that are compatible with and supported by Grafana Ale
- [Graphite]({{< relref "../../datasources/graphite/" >}})
- [InfluxDB]({{< relref "../../datasources/influxdb/" >}})
- [Loki]({{< relref "../../datasources/loki/" >}})
- [Microsoft SQL Server (MSSQL)]({{< relref "../../datasources/mssql/" >}})
- ]Microsoft SQL Server (MSSQL)]({{< relref "../../datasources/mssql/" >}})
- [MySQL]({{< relref "../../datasources/mysql/" >}})
- [Open TSDB]({{< relref "../../datasources/opentsdb/" >}})
- [PostgreSQL]({{< relref "../../datasources/postgres/" >}})

View File

@@ -34,8 +34,8 @@ To use images in notifications, Grafana must be set up to use [image rendering](
If Grafana has been set up to use [image rendering]({{< relref "../setup-grafana/image-rendering/" >}}) images in notifications can be turned on via the `capture` option in `[unified_alerting.screenshots]`:
# Enable screenshots in notifications. This option requires the Grafana Image Renderer plugin.
# For more information on configuration options, refer to [rendering].
# Enable screenshots in notifications. This option requires a remote HTTP image rendering service. Please
# see [rendering] for further configuration options.
capture = true
It is recommended that `max_concurrent_screenshots` is set to a value that is less than or equal to `concurrent_render_request_limit`. The default value for both `max_concurrent_screenshots` and `concurrent_render_request_limit` is `5`:
@@ -71,7 +71,7 @@ Images in notifications are supported in the following notifiers and additional
| Opsgenie | No | Yes |
| Pagerduty | No | Yes |
| Prometheus Alertmanager | No | No |
| Pushover | Yes | No |
| Pushover | No | No |
| Sensu Go | No | No |
| Slack | No | Yes |
| Telegram | No | No |

View File

@@ -30,7 +30,6 @@ Read more about the available dimensions in the [CloudWatch Metrics and Dimensio
| `EC2 Instance Attributes` | Returns a list of attributes matching the specified `region`, `attribute_name`, and `filters`. |
| `Resource ARNs` | Returns a list of ARNs matching the specified `region`, `resource_type` and `tags`. |
| `Statistics` | Returns a list of all the standard statistics. |
| `LogGroups` | Returns a list of all log groups matching the specified `region`. |
For details about the metrics CloudWatch provides, please refer to the [CloudWatch documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/CW_Support_For_AWS.html).

View File

@@ -18,9 +18,7 @@ weight: 999
# Deprecated Application Insights and Insights Analytics
Application Insights and Insights Analytics are two ways to query the same Azure Application Insights data, which can also be queried from Metrics and Logs. In Grafana 8.0, Application Insights and Insights Analytics were deprecated and made read-only in favor of querying this data through Metrics and Logs.
These query methods were completely removed in Grafana 9.0.
Application Insights and Insights Analytics are two ways to query the same Azure Application Insights data, which can also be queried from Metrics and Logs. In Grafana 8.0, Application Insights and Insights Analytics are deprecated and made read-only in favor of querying this data through Metrics and Logs. Existing queries will continue to work, but you cannot edit them. New panels are not able to use Application Insights or Insights Analytics.
Azure Monitor Metrics and Azure Monitor Logs do not use Application Insights API keys, so make sure the data source is configured with an Azure AD app registration that has access to Application Insights.

View File

@@ -471,7 +471,7 @@ Content-Type: application/json
```http
POST /api/admin/ldap/reload HTTP/1.1
Accept: application/json
Accept: application/json
Content-Type: application/json
```

View File

@@ -15,7 +15,7 @@ title: 'Alerting HTTP API '
# Alerting API
> **Note:** This topic is relevant for the [legacy dashboard alerts](https://grafana.com/docs/grafana/v8.5/alerting/old-alerting/) only.
> **Note:** This topic is relevant for the [legacy dashboard alerts]({{< relref "../../old-alerting/" >}}) only.
You can find Grafana alerting API specification details [here](https://editor.swagger.io/?url=https://raw.githubusercontent.com/grafana/grafana/main/pkg/services/ngalert/api/tooling/post.json). Also, refer to [Grafana alerting alerts documentation]({{< relref "../../alerting/" >}}) for details on how to create and manage new alerts.

View File

@@ -185,7 +185,7 @@ Content-Type: application/json
```
**Example response**:
**Example response**:
```http
HTTP/1.1 200

View File

@@ -19,9 +19,7 @@ This functionality is similar to the panel inspector tasks [Inspect query perfor
## Query history
Query history is a list of queries that you used in Explore. The history is stored in the Grafana database and it is not shared with other users. The retention period for queries in history is two weeks. Queries older than two weeks are automatically deleted. To open and interact with your history, click the **Query history** button in Explore.
> **Note**: Starred queries are not subject to the two weeks retention period and they are not deleted.
Query history is a list of queries that you have used in Explore. The history is local to your browser and is not shared. To open and interact with your history, click the **Query history** button in Explore.
### View query history
@@ -45,6 +43,10 @@ By default, query history shows you the most recent queries. You can sort your h
1. Select one of the following options:
- Newest first
- Oldest first
- Data source A-Z
- Data source Z-A
> **Note:** If you are in split mode, then the chosen sorting mode applies only to the active panel.
### Filter query history
@@ -59,6 +61,8 @@ In **Query history** tab it is also possible to filter queries by date using the
- By dragging top handle, adjust start date.
- By dragging top handle, adjust end date.
> **Note:** If you are in split mode, filters are applied only to your currently active panel.
### Search in query history
You can search in your history across queries and your comments. Search is possible for queries in the Query history tab and Starred tab.
@@ -70,9 +74,12 @@ You can search in your history across queries and your comments. Search is possi
You can customize the query history in the Settings tab. Options are described in the table below.
| Setting | Default value |
| ----------------------------- | ----------------- |
| Change the default active tab | Query history tab |
| Setting | Default value |
| ------------------------------------------------------------- | --------------------------------------- |
| Period of time for which Grafana will save your query history | 1 week |
| Change the default active tab | Query history tab |
| Only show queries for data source currently active in Explore | True |
| Clear query history | Permanently deletes all stored queries. |
> **Note:** Query history settings are global, and applied to both panels in split mode.

View File

@@ -27,15 +27,9 @@ Grafana can be installed on many different operating systems. For a list of the
To sign in to Grafana for the first time:
1. Open your web browser and go to http://localhost:3000/.
The default HTTP port that Grafana listens to is `3000` unless you have configured a different port.
1. On the sign-in page, enter `admin` for the username and password.
1. Click **Sign in**.
If successful, you will see a prompt to change the password.
1. Open your web browser and go to http://localhost:3000/. The default HTTP port that Grafana listens to is `3000` unless you have configured a different port.
1. On the signin page, enter `admin` for username and password.
1. Click **Sign in**. If successful, you will see a prompt to change the password.
1. Click **OK** on the prompt and change your password.
> **Note:** We strongly recommend that you change the default administrator password.
@@ -47,10 +41,7 @@ To create your first dashboard:
1. Click the **New dashboard** item under the **Dashboards** icon in the side menu.
1. On the dashboard, click **Add an empty panel**.
1. In the New dashboard/Edit panel view, go to the **Query** tab.
1. Configure your [query]({{< relref "../panels/query-a-data-source/add-a-query/" >}}) by selecting `-- Grafana --` from the data source selector.
This generates the Random Walk dashboard.
1. Configure your [query]({{< relref "../panels/query-a-data-source/add-a-query/" >}}) by selecting `-- Grafana --` from the data source selector. This generates the Random Walk dashboard.
1. Click the **Save** icon in the top right corner of your screen to save the dashboard.
1. Add a descriptive name, and then click **Save**.

View File

@@ -1,9 +1,9 @@
---
aliases:
- /docs/grafana/latest/panels/working-with-panels/configure-legend/
- /docs/grafana/latest/visualizations/configure-legend/
- /docs/sources/panels/working-with-panels/configure-legend/
title: Configure a legend
weight: 1300
weight: 70
---
# Configure a legend
@@ -41,7 +41,13 @@ This topic currently applies to the following visualizations:
As way to add more context to a visualization, you can add series data values to a legend. You can add as many values as you'd like; after you apply your changes, you can horizontally scroll the legend to see all values.
1. Edit a panel.
### Before you begin
- Add a panel
**To add values to a legend**:
1. Open a panel.
1. In the panel display options pane, locate the **Legend** section.
@@ -55,7 +61,13 @@ As way to add more context to a visualization, you can add series data values to
By default, Grafana specifies the color of your series data, which you can change.
1. Edit a panel.
### Before you begin
- Add a panel
**To change a series color, perform the following steps**:
1. Open the panel.
1. In the legend, click the color bar associated with the series.
@@ -67,7 +79,7 @@ By default, Grafana specifies the color of your series data, which you can chang
## Sort series
You can change legend mode to **Table** and choose [calculations]({{< relref "../../panels/calculation-types/" >}}) to be displayed in the legend. Click the calculation name header in the legend table to sort the values in the table in ascending or descending order.
Change legend mode to **Table** and choose [calculations]({{< relref "../calculation-types/" >}}) to be displayed in the legend. Click the calculation name header in the legend table to sort the values in the table in ascending or descending order.
The sort order affects the positions of the bars in the Bar chart panel as well as the order of stacked series in the Time series and Bar chart panels.

View File

@@ -19,7 +19,7 @@ You can place any panel in any location you want and controls its size. The chan
1. Hover your cursor over the panel, and click-and-drag the panel to its new location.
1. To resize a panel, click the zoom in (+) and zoom out (-) icons.
1. To resize a penal, click the zoom in (+) and zoom out (-) icons.
![](/static/img/docs/animated_gifs/drag_drop.gif)

View File

@@ -10,9 +10,6 @@ weight: 10000
Here you can find detailed release notes that list everything that is included in every release as well as notices
about deprecations, breaking changes as well as changes that relate to plugin development.
- [Release notes for 9.0.6]({{< relref "release-notes-9-0-6" >}})
- [Release notes for 9.0.5]({{< relref "release-notes-9-0-5" >}})
- [Release notes for 9.0.4]({{< relref "release-notes-9-0-4" >}})
- [Release notes for 9.0.3]({{< relref "release-notes-9-0-3" >}})
- [Release notes for 9.0.2]({{< relref "release-notes-9-0-2" >}})
- [Release notes for 9.0.1]({{< relref "release-notes-9-0-1" >}})

View File

@@ -1,32 +0,0 @@
+++
title = "Release notes for Grafana 9.0.4"
hide_menu = true
+++
<!-- Auto generated by update changelog github action -->
# Release notes for Grafana 9.0.4
### Features and enhancements
- **Browse/Search:** Make browser back work properly when visiting Browse or search. [#52271](https://github.com/grafana/grafana/pull/52271), [@torkelo](https://github.com/torkelo)
- **Logs:** Improve getLogRowContext API. [#52130](https://github.com/grafana/grafana/pull/52130), [@gabor](https://github.com/gabor)
- **Loki:** Improve handling of empty responses. [#52397](https://github.com/grafana/grafana/pull/52397), [@gabor](https://github.com/gabor)
- **Plugins:** Always validate root URL if specified in signature manfiest. [#52332](https://github.com/grafana/grafana/pull/52332), [@wbrowne](https://github.com/wbrowne)
- **Preferences:** Get home dashboard from teams. [#52225](https://github.com/grafana/grafana/pull/52225), [@sakjur](https://github.com/sakjur)
- **SQLStore:** Support Upserting multiple rows. [#52228](https://github.com/grafana/grafana/pull/52228), [@joeblubaugh](https://github.com/joeblubaugh)
- **Traces:** Add more template variables in Tempo & Zipkin. [#52306](https://github.com/grafana/grafana/pull/52306), [@joey-grafana](https://github.com/joey-grafana)
- **Traces:** Remove serviceMap feature flag. [#52375](https://github.com/grafana/grafana/pull/52375), [@joey-grafana](https://github.com/joey-grafana)
### Bug fixes
- **Access Control:** Fix missing folder permissions. [#52410](https://github.com/grafana/grafana/pull/52410), [@IevaVasiljeva](https://github.com/IevaVasiljeva)
- **Access control:** Fix org user removal for OSS users. [#52473](https://github.com/grafana/grafana/pull/52473), [@IevaVasiljeva](https://github.com/IevaVasiljeva)
- **Alerting:** Fix Slack notification preview. [#50230](https://github.com/grafana/grafana/pull/50230), [@ekrucio](https://github.com/ekrucio)
- **Alerting:** Fix Slack push notifications. [#52391](https://github.com/grafana/grafana/pull/52391), [@grobinson-grafana](https://github.com/grobinson-grafana)
- **Alerting:** Fixes slack push notifications. [#50267](https://github.com/grafana/grafana/pull/50267), [@jgillick](https://github.com/jgillick)
- **Alerting:** Preserve new-lines from custom email templates in rendered email. [#52253](https://github.com/grafana/grafana/pull/52253), [@alexweav](https://github.com/alexweav)
- **Insights:** Fix dashboard and data source insights pages. (Enterprise)
- **Log:** Fix text logging for unsupported types. [#51306](https://github.com/grafana/grafana/pull/51306), [@papagian](https://github.com/papagian)
- **Loki:** Fix `show context` not working in some occasions. [#52458](https://github.com/grafana/grafana/pull/52458), [@svennergr](https://github.com/svennergr)
- **Loki:** Fix incorrect TopK value type in query builder. [#52226](https://github.com/grafana/grafana/pull/52226), [@ivanahuckova](https://github.com/ivanahuckova)

View File

@@ -1,26 +0,0 @@
+++
title = "Release notes for Grafana 9.0.5"
hide_menu = true
+++
<!-- Auto generated by update changelog github action -->
# Release notes for Grafana 9.0.5
### Features and enhancements
- **Access control:** Show dashboard settings to users who can edit dashboard. [#52535](https://github.com/grafana/grafana/pull/52535), [@grafanabot](https://github.com/grafanabot)
- **Alerting:** Allow the webhook notifier to support a custom Authorization header. [#52515](https://github.com/grafana/grafana/pull/52515), [@gotjosh](https://github.com/gotjosh)
- **Chore:** Upgrade to Go version 1.17.12. [#52523](https://github.com/grafana/grafana/pull/52523), [@sakjur](https://github.com/sakjur)
- **Plugins:** Add signature wildcard globbing for dedicated private plugin type. [#52163](https://github.com/grafana/grafana/pull/52163), [@wbrowne](https://github.com/wbrowne)
- **Prometheus:** Don't show errors from unsuccessful API checks like rules or exemplar checks. [#52193](https://github.com/grafana/grafana/pull/52193), [@darrenjaneczek](https://github.com/darrenjaneczek)
### Bug fixes
- **Access control:** Allow organisation admins to add existing users to org (#51668). [#52553](https://github.com/grafana/grafana/pull/52553), [@vtorosyan](https://github.com/vtorosyan)
- **Alerting:** Fix alert panel instance-based rules filtering. [#52583](https://github.com/grafana/grafana/pull/52583), [@konrad147](https://github.com/konrad147)
- **Apps:** Fixes navigation between different app plugin pages. [#52571](https://github.com/grafana/grafana/pull/52571), [@torkelo](https://github.com/torkelo)
- **Cloudwatch:** Upgrade grafana-aws-sdk to fix auth issue with secret keys. [#52420](https://github.com/grafana/grafana/pull/52420), [@sarahzinger](https://github.com/sarahzinger)
- **Grafana/toolkit:** Fix incorrect image and font generation for plugin builds. [#52661](https://github.com/grafana/grafana/pull/52661), [@academo](https://github.com/academo)
- **Loki:** Fix `show context` not working in some occasions. [#52458](https://github.com/grafana/grafana/pull/52458), [@svennergr](https://github.com/svennergr)
- **RBAC:** Fix permissions on dashboards and folders created by anonymous users. [#52615](https://github.com/grafana/grafana/pull/52615), [@gamab](https://github.com/gamab)

View File

@@ -1,18 +0,0 @@
+++
title = "Release notes for Grafana 9.0.6"
hide_menu = true
+++
<!-- Auto generated by update changelog github action -->
# Release notes for Grafana 9.0.6
### Features and enhancements
- **Access Control:** Allow org admins to invite new users to their organization. [#52904](https://github.com/grafana/grafana/pull/52904), [@IevaVasiljeva](https://github.com/IevaVasiljeva)
### Bug fixes
- **Grafana/toolkit:** Fix incorrect image and font generation for plugin builds. [#52927](https://github.com/grafana/grafana/pull/52927), [@academo](https://github.com/academo)
- **Prometheus:** Fix adding of multiple values for regex operator. [#52978](https://github.com/grafana/grafana/pull/52978), [@ivanahuckova](https://github.com/ivanahuckova)
- **UI/Card:** Fix card items always having pointer cursor. [#52809](https://github.com/grafana/grafana/pull/52809), [@gillesdemey](https://github.com/gillesdemey)

View File

@@ -324,7 +324,7 @@ Sets the maximum amount of time a connection may be reused. The default is 14400
### locking_attempt_timeout_sec
For "mysql", if the `migrationLocking` feature toggle is set, specify the time (in seconds) to wait before failing to lock the database for the migrations. Default is 0.
For "mysql", if `lockingMigration` feature toggle is set, specify the time (in seconds) to wait before failing to lock the database for the migrations. Default is 0.
### log_queries
@@ -675,8 +675,7 @@ Default is `false`.
Set to `true` to automatically add new users to the main organization
(id 1). When set to `false`, new users automatically cause a new
organization to be created for that new user. The organization will be
created even if the `allow_org_create` setting is set to `false`. Default is `true`.
organization to be created for that new user. Default is `true`.
### auto_assign_org_id
@@ -1204,11 +1203,11 @@ The interval string is a possibly signed sequence of decimal numbers, followed b
### ha_listen_address
Listen IP address and port to receive unified alerting messages for other Grafana instances. The port is used for both TCP and UDP. It is assumed other Grafana instances are also running on the same port. The default value is `0.0.0.0:9094`.
Listen address/hostname and port to receive unified alerting messages for other Grafana instances. The port is used for both TCP and UDP. It is assumed other Grafana instances are also running on the same port. The default value is `0.0.0.0:9094`.
### ha_advertise_address
Explicit IP address and port to advertise other Grafana instances. The port is used for both TCP and UDP.
Explicit address/hostname and port to advertise other Grafana instances. The port is used for both TCP and UDP.
### ha_peers

View File

@@ -6,4 +6,4 @@ title: Stack series link
## Axis
For full instructions, refer to [Change axis display]({{< relref "../../visualizations/time-series/change-axis-display/" >}}).
For full instructions, refer to [Change axis display]({{< relref "../time-series/change-axis-display/" >}}).

View File

@@ -6,4 +6,4 @@ title: Stack series link
### Stack series
For full instructions, refer to [Graph stacked time series]({{< relref "../../visualizations/time-series/graph-time-series-stacking/" >}}).
For full instructions, refer to [Graph stacked time series]({{< relref "../time-series/graph-time-series-stacking/" >}}).

View File

@@ -21,7 +21,7 @@ This panel visualization allows you to graph categorical data.
## Supported data formats
Only one data frame is supported and it must have at least one string field that will be used as the category for an X or Y axis and one or more numerical fields.
Only one data frame is supported and it needs to have at least one string field that will be used as the category for an X or Y axis and one or more numerical fields.
Example:
@@ -107,8 +107,6 @@ Gradient color is generated based on the hue of the line color.
Choose which of the [standard calculations]({{< relref "../panels/calculation-types/" >}}) to show in the legend. You can have more than one.
For more information about the legend, refer to [Configure a legend](../configure-legend/).
## Text size
Enter a **Value** to change the size of the text on your bar chart.

View File

@@ -80,7 +80,5 @@ The following example shows a pie chart with **Name** and **Percent** labels dis
Select values to display in the legend. You can select more than one.
- **Percent:** The percentage of the whole.
- **Value:** The raw numerical value.
For more information about the legend, refer to [Configure a legend](../configure-legend/).
**Percent -** The percentage of the whole.
**Value -** The raw numerical value.

View File

@@ -33,8 +33,6 @@ These options are available whether you are graphing your time series as lines,
Choose which of the [standard calculations]({{< relref "../../panels/calculation-types/" >}}) to show in the legend. You can have more than one.
For more information about the legend, refer to [Configure a legend](../configure-legend/).
## Graph styles
Use these options to choose how to display your time series data.

View File

@@ -74,7 +74,7 @@ You can now dynamically apply value filters to any table column. This option can
{{< figure src="/static/img/docs/v72/table_column_filters.png" max-width="800px" caption="Table column filters" >}}
[Filter table columns]({{< relref "../visualizations/table/#filter-table-columns" >}}) has been added as a result of this feature.
[Filter table columns]({{< relref "../visualizations/table/filter-table-columns/" >}}) has been added as a result of this feature.
### New field override selection options

View File

@@ -114,6 +114,8 @@ You can now provide detailed information to alert notification recipients by inj
{{< figure src="/static/img/docs/alerting/alert-notification-template-7-4.png" max-width="700px" caption="Variable support in alert notifications" >}}
For more information, refer to the [alert notification docs]({{< ref "/docs/grafana/v8.5/alerting/old-alerting/add-notification-template/" >}}).
### Content security policy support
We have added support for [Content Security Policy (CSP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP), a layer of security that helps detect and mitigate certain types of attacks, including Cross Site Scripting (XSS) and data injection attacks.

View File

@@ -4,5 +4,5 @@
"packages": [
"packages/*"
],
"version": "9.0.7"
"version": "9.0.4"
}

View File

@@ -3,7 +3,7 @@
"license": "AGPL-3.0-only",
"private": true,
"name": "grafana",
"version": "9.0.7",
"version": "9.0.4",
"repository": "github:grafana/grafana",
"scripts": {
"api-tests": "jest --notify --watch --config=devenv/e2e-api-tests/jest.js",
@@ -330,7 +330,7 @@
"logfmt": "^1.3.2",
"lru-cache": "7.10.1",
"memoize-one": "6.0.0",
"moment": "2.29.4",
"moment": "2.29.3",
"moment-timezone": "0.5.34",
"monaco-editor": "^0.31.1",
"monaco-promql": "1.7.4",

View File

@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/data",
"version": "9.0.7",
"version": "9.0.4",
"description": "Grafana Data Library",
"keywords": [
"typescript"
@@ -22,14 +22,14 @@
},
"dependencies": {
"@braintree/sanitize-url": "6.0.0",
"@grafana/schema": "9.0.7",
"@grafana/schema": "9.0.4",
"@types/d3-interpolate": "^1.4.0",
"d3-interpolate": "1.4.0",
"date-fns": "2.28.0",
"eventemitter3": "4.0.7",
"lodash": "4.17.21",
"marked": "4.0.16",
"moment": "2.29.4",
"moment": "2.29.3",
"moment-timezone": "0.5.34",
"ol": "6.14.1",
"papaparse": "5.3.2",

View File

@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/e2e-selectors",
"version": "9.0.7",
"version": "9.0.4",
"description": "Grafana End-to-End Test Selectors Library",
"keywords": [
"cli",

View File

@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/e2e",
"version": "9.0.7",
"version": "9.0.4",
"description": "Grafana End-to-End Test Library",
"keywords": [
"cli",
@@ -48,7 +48,7 @@
"@babel/core": "7.17.8",
"@babel/preset-env": "7.17.10",
"@cypress/webpack-preprocessor": "5.11.1",
"@grafana/e2e-selectors": "9.0.7",
"@grafana/e2e-selectors": "9.0.4",
"@grafana/tsconfig": "^1.2.0-rc1",
"@mochajs/json-file-reporter": "^1.2.0",
"babel-loader": "8.2.5",

View File

@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/runtime",
"version": "9.0.7",
"version": "9.0.4",
"description": "Grafana Runtime Library",
"keywords": [
"grafana",
@@ -22,9 +22,9 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@grafana/data": "9.0.7",
"@grafana/e2e-selectors": "9.0.7",
"@grafana/ui": "9.0.7",
"@grafana/data": "9.0.4",
"@grafana/e2e-selectors": "9.0.4",
"@grafana/ui": "9.0.4",
"@sentry/browser": "6.19.7",
"history": "4.10.1",
"lodash": "4.17.21",

View File

@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/schema",
"version": "9.0.7",
"version": "9.0.4",
"description": "Grafana Schema Library",
"keywords": [
"typescript"

View File

@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/toolkit",
"version": "9.0.7",
"version": "9.0.4",
"description": "Grafana Toolkit",
"keywords": [
"grafana",
@@ -38,10 +38,10 @@
"@babel/preset-env": "^7.16.11",
"@babel/preset-react": "^7.16.7",
"@babel/preset-typescript": "^7.16.7",
"@grafana/data": "9.0.7",
"@grafana/data": "9.0.4",
"@grafana/eslint-config": "^4.0.0",
"@grafana/tsconfig": "^1.2.0-rc1",
"@grafana/ui": "9.0.7",
"@grafana/ui": "9.0.4",
"@jest/core": "27.5.1",
"@types/command-exists": "^1.2.0",
"@types/eslint": "8.4.1",

View File

@@ -5,7 +5,7 @@ import rimrafCallback from 'rimraf';
import { promisify } from 'util';
import { getPluginId } from '../../config/utils/getPluginId';
import { assertRootUrlIsValid, getPluginJson } from '../../config/utils/pluginValidation';
import { getPluginJson } from '../../config/utils/pluginValidation';
import {
getJobFolder,
writeJobStats,
@@ -140,8 +140,7 @@ const packagePluginRunner: TaskRunner<PluginCIOptions> = async ({ signatureType,
if (signatureType) {
manifest.signatureType = signatureType;
}
if (rootUrls && rootUrls.length > 0) {
rootUrls.forEach(assertRootUrlIsValid);
if (rootUrls) {
manifest.rootUrls = rootUrls;
}
const signedManifest = await signManifest(manifest);

View File

@@ -1,6 +1,5 @@
import path from 'path';
import { assertRootUrlIsValid } from '../../config/utils/pluginValidation';
import { buildManifest, signManifest, saveManifest } from '../../plugins/manifest';
import { getToolkitVersion } from './plugin.utils';
@@ -22,8 +21,7 @@ const pluginSignRunner: TaskRunner<PluginSignOptions> = async ({ signatureType,
if (signatureType) {
manifest.signatureType = signatureType;
}
if (rootUrls && rootUrls.length > 0) {
rootUrls.forEach(assertRootUrlIsValid);
if (rootUrls) {
manifest.rootUrls = rootUrls;
}

View File

@@ -36,11 +36,3 @@ export const getPluginJson = (path: string): PluginMeta => {
return pluginJson as PluginMeta;
};
export const assertRootUrlIsValid = (rootUrl: string) => {
try {
new URL(rootUrl);
} catch (err) {
throw new Error(`${rootUrl} is not a valid URL`);
}
};

View File

@@ -1,4 +1,4 @@
import { getStylesheetEntries } from './loaders';
import { getStylesheetEntries, hasThemeStylesheets } from './loaders';
describe('Loaders', () => {
describe('stylesheet helpers', () => {
@@ -22,5 +22,28 @@ describe('Loaders', () => {
expect(result).toThrow();
});
});
describe('hasThemeStylesheets', () => {
it('throws when only one theme file is defined', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation();
const result = () => {
hasThemeStylesheets(`${__dirname}/../mocks/stylesheetsSupport/missing-theme-file`);
};
expect(result).toThrow();
errorSpy.mockRestore();
});
it('returns false when no theme files present', () => {
const result = hasThemeStylesheets(`${__dirname}/../mocks/stylesheetsSupport/no-theme-files`);
expect(result).toBeFalsy();
});
it('returns true when theme files present', () => {
const result = hasThemeStylesheets(`${__dirname}/../mocks/stylesheetsSupport/ok`);
expect(result).toBeTruthy();
});
});
});
});

View File

@@ -33,6 +33,39 @@ export const getStylesheetEntries = (root: string = process.cwd()) => {
return entries;
};
export const hasThemeStylesheets = (root: string = process.cwd()) => {
const stylesheetsPaths = getStylesheetPaths(root);
const stylesheetsSummary: boolean[] = [];
const result = stylesheetsPaths.reduce((acc, current) => {
if (fs.existsSync(`${current}.css`) || fs.existsSync(`${current}.scss`)) {
stylesheetsSummary.push(true);
return acc && true;
} else {
stylesheetsSummary.push(false);
return false;
}
}, true);
const hasMissingStylesheets = stylesheetsSummary.filter((s) => s).length === 1;
// seems like there is one theme file defined only
if (result === false && hasMissingStylesheets) {
console.error('\nWe think you want to specify theme stylesheet, but it seems like there is something missing...');
stylesheetsSummary.forEach((s, i) => {
if (s) {
console.log(stylesheetsPaths[i], 'discovered');
} else {
console.log(stylesheetsPaths[i], 'missing');
}
});
throw new Error('Stylesheet missing!');
}
return result;
};
export const getStyleLoaders = () => {
const extractionLoader = {
loader: MiniCssExtractPlugin.loader,
@@ -106,21 +139,34 @@ export const getStyleLoaders = () => {
};
export const getFileLoaders = () => {
const shouldExtractCss = hasThemeStylesheets();
return [
{
test: /\.(png|jpe?g|gif|svg)$/,
type: 'asset/resource',
generator: {
publicPath: `public/plugins/${getPluginId()}/img/`,
outputPath: 'img/',
},
use: [
shouldExtractCss
? {
loader: require.resolve('file-loader'),
options: {
outputPath: '/',
name: '[path][name].[ext]',
},
}
: // When using single css import images are inlined as base64 URIs in the result bundle
{
loader: 'url-loader',
},
],
},
{
test: /\.(woff|woff2|eot|ttf|otf)(\?v=\d+\.\d+\.\d+)?$/,
type: 'asset/resource',
generator: {
publicPath: `public/plugins/${getPluginId()}/fonts/`,
outputPath: 'fonts/',
loader: require.resolve('file-loader'),
options: {
// Keep publicPath relative for host.com/grafana/ deployments
publicPath: `public/plugins/${getPluginId()}/fonts`,
outputPath: 'fonts',
name: '[name].[ext]',
},
},
];

View File

@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/ui",
"version": "9.0.7",
"version": "9.0.4",
"description": "Grafana Components Library",
"keywords": [
"grafana",
@@ -33,9 +33,9 @@
"@emotion/css": "11.9.0",
"@emotion/react": "11.9.0",
"@grafana/aws-sdk": "0.0.36",
"@grafana/data": "9.0.7",
"@grafana/e2e-selectors": "9.0.7",
"@grafana/schema": "9.0.7",
"@grafana/data": "9.0.4",
"@grafana/e2e-selectors": "9.0.4",
"@grafana/schema": "9.0.4",
"@grafana/slate-react": "0.22.10-grafana",
"@monaco-editor/react": "4.3.1",
"@popperjs/core": "2.11.5",
@@ -58,7 +58,7 @@
"jquery": "3.6.0",
"lodash": "4.17.21",
"memoize-one": "6.0.0",
"moment": "2.29.4",
"moment": "2.29.3",
"monaco-editor": "^0.31.1",
"ol": "6.14.1",
"prismjs": "1.28.0",

View File

@@ -67,7 +67,7 @@ export const CardContainer = ({
};
export const getCardContainerStyles = stylesFactory(
(theme: GrafanaTheme2, disabled = false, disableHover = false, isSelected) => {
(theme: GrafanaTheme2, disabled = false, disableHover = false, isSelected = false) => {
const isSelectable = isSelected !== undefined;
return {

View File

@@ -1,13 +0,0 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { PageToolbar } from '..';
describe('PageToolbar', () => {
it('renders left items when title is not set', () => {
const leftItemContent = 'Left Item!';
render(<PageToolbar leftItems={[<div key="left-item">{leftItemContent}</div>]} />);
expect(screen.getByText(leftItemContent)).toBeInTheDocument();
});
});

View File

@@ -15,7 +15,6 @@ import { IconButton } from '../IconButton/IconButton';
export interface Props {
pageIcon?: IconName;
title?: string;
section?: string;
parent?: string;
onGoBack?: () => void;
titleHref?: string;
@@ -31,7 +30,6 @@ export interface Props {
export const PageToolbar: FC<Props> = React.memo(
({
title,
section,
parent,
pageIcon,
onGoBack,
@@ -61,78 +59,63 @@ export const PageToolbar: FC<Props> = React.memo(
className
);
const titleEl = (
<>
<span className={styles.noLinkTitle}>{title}</span>
{section && <span className={styles.pre}> / {section}</span>}
</>
);
return (
<nav className={mainStyle} aria-label={ariaLabel}>
<div className={styles.leftWrapper}>
{pageIcon && !onGoBack && (
<div className={styles.pageIcon}>
<Icon name={pageIcon} size="lg" aria-hidden />
</div>
{pageIcon && !onGoBack && (
<div className={styles.pageIcon}>
<Icon name={pageIcon} size="lg" aria-hidden />
</div>
)}
{onGoBack && (
<div className={styles.pageIcon}>
<IconButton
name="arrow-left"
tooltip="Go back (Esc)"
tooltipPlacement="bottom"
size="xxl"
aria-label={selectors.components.BackButton.backArrow}
onClick={onGoBack}
/>
</div>
)}
<nav aria-label="Search links" className={styles.navElement}>
{parent && parentHref && (
<>
<Link
aria-label={`Search dashboard in the ${parent} folder`}
className={cx(styles.titleText, styles.parentLink, styles.titleLink)}
href={parentHref}
>
{parent} <span className={styles.parentIcon}></span>
</Link>
{titleHref && (
<span className={cx(styles.titleText, styles.titleDivider, styles.parentLink)} aria-hidden>
/
</span>
)}
</>
)}
{onGoBack && (
<div className={styles.pageIcon}>
<IconButton
name="arrow-left"
tooltip="Go back (Esc)"
tooltipPlacement="bottom"
size="xxl"
aria-label={selectors.components.BackButton.backArrow}
onClick={onGoBack}
/>
</div>
{title && titleHref && (
<h1 className={styles.h1Styles}>
<Link
aria-label="Search dashboard by name"
className={cx(styles.titleText, styles.titleLink)}
href={titleHref}
>
{title}
</Link>
</h1>
)}
<nav aria-label="Search links" className={styles.navElement}>
{parent && parentHref && (
<>
<Link
aria-label={`Search dashboard in the ${parent} folder`}
className={cx(styles.titleText, styles.parentLink, styles.titleLink)}
href={parentHref}
>
{parent} <span className={styles.parentIcon}></span>
</Link>
{titleHref && (
<span className={cx(styles.titleText, styles.titleDivider, styles.parentLink)} aria-hidden>
/
</span>
)}
</>
)}
{title && !titleHref && <h1 className={styles.titleText}>{title}</h1>}
</nav>
{leftItems?.map((child, index) => (
<div className={styles.leftActionItem} key={index}>
{child}
</div>
))}
{(title || leftItems?.length) && (
<div className={styles.titleWrapper}>
{title && (
<h1 className={styles.h1Styles}>
{titleHref ? (
<Link
aria-label="Search dashboard by name"
className={cx(styles.titleText, styles.titleLink)}
href={titleHref}
>
{titleEl}
</Link>
) : (
<div className={styles.titleText}>{titleEl}</div>
)}
</h1>
)}
{leftItems?.map((child, index) => (
<div className={styles.leftActionItem} key={index}>
{child}
</div>
))}
</div>
)}
</nav>
</div>
<div className={styles.spacer} />
{React.Children.toArray(children)
.filter(Boolean)
.map((child, index) => {
@@ -153,11 +136,21 @@ const getStyles = (theme: GrafanaTheme2) => {
const { spacing, typography } = theme;
const focusStyle = getFocusStyles(theme);
const titleStyles = css`
font-size: ${typography.size.lg};
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
margin: 0;
max-width: 240px;
border-radius: 2px;
@media ${styleMixins.mediaUp(theme.v1.breakpoints.xl)} {
max-width: unset;
}
`;
return {
pre: css`
white-space: pre;
`,
toolbar: css`
align-items: center;
background: ${theme.colors.background.canvas};
@@ -166,9 +159,7 @@ const getStyles = (theme: GrafanaTheme2) => {
justify-content: flex-end;
padding: ${theme.spacing(1.5, 2)};
`,
leftWrapper: css`
display: flex;
flex-wrap: nowrap;
spacer: css`
flex-grow: 1;
`,
pageIcon: css`
@@ -179,38 +170,24 @@ const getStyles = (theme: GrafanaTheme2) => {
align-items: center;
}
`,
noLinkTitle: css`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`,
titleWrapper: css`
display: flex;
flex-grow: 1;
margin: 0;
align-items: center;
min-width: 0;
overflow: hidden;
`,
navElement: css`
display: flex;
flex-grow: 1;
align-items: center;
max-width: calc(100vw - 78px);
`,
h1Styles: css`
margin: 0;
line-height: inherit;
width: 300px;
max-width: min-content;
flex-grow: 1;
display: flex;
`,
parentIcon: css`
margin-left: ${theme.spacing(0.5)};
`,
titleText: css`
display: flex;
font-size: ${typography.size.lg};
margin: 0;
border-radius: 2px;
`,
titleText: titleStyles,
titleLink: css`
&:focus-visible {
${focusStyle}

View File

@@ -1,6 +1,6 @@
{
"name": "@jaegertracing/jaeger-ui-components",
"version": "9.0.7",
"version": "9.0.4",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -28,10 +28,10 @@
},
"dependencies": {
"@emotion/css": "11.9.0",
"@grafana/data": "9.0.7",
"@grafana/e2e-selectors": "9.0.7",
"@grafana/runtime": "9.0.7",
"@grafana/ui": "9.0.7",
"@grafana/data": "9.0.4",
"@grafana/e2e-selectors": "9.0.4",
"@grafana/runtime": "9.0.4",
"@grafana/ui": "9.0.4",
"chance": "^1.0.10",
"classnames": "^2.2.5",
"combokeys": "^3.0.0",
@@ -43,7 +43,7 @@
"lodash": "4.17.21",
"lru-memoize": "^1.1.0",
"memoize-one": "6.0.0",
"moment": "2.29.4",
"moment": "2.29.3",
"moment-timezone": "0.5.34",
"prop-types": "15.8.1",
"react": "17.0.2",

View File

@@ -33,13 +33,13 @@ RUN apk add --no-cache openssl --repository=http://dl-cdn.alpinelinux.org/alpine
# Oracle Support for x86_64 only
RUN if [ `arch` = "x86_64" ]; then \
apk add --no-cache libaio libnsl && \
ln -s /usr/lib/libnsl.so.2 /usr/lib/libnsl.so.1 && \
wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.35-r0/glibc-2.35-r0.apk \
-O /tmp/glibc-2.35-r0.apk && \
wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.35-r0/glibc-bin-2.35-r0.apk \
-O /tmp/glibc-bin-2.35-r0.apk && \
apk add --no-cache --allow-untrusted /tmp/glibc-2.35-r0.apk /tmp/glibc-bin-2.35-r0.apk && \
rm -f /lib64/ld-linux-x86-64.so.2 && \
ln -s /usr/glibc-compat/lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 && \
rm -f /tmp/glibc-2.35-r0.apk && \
rm -f /tmp/glibc-bin-2.35-r0.apk && \
rm -f /lib/ld-linux-x86-64.so.2 && \

View File

@@ -12,7 +12,7 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/channels_config"
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
"github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
@@ -197,7 +197,7 @@ func (hs *HTTPServer) GetAlert(c *models.ReqContext) response.Response {
func (hs *HTTPServer) GetAlertNotifiers(ngalertEnabled bool) func(*models.ReqContext) response.Response {
return func(_ *models.ReqContext) response.Response {
if ngalertEnabled {
return response.JSON(http.StatusOK, channels_config.GetAvailableNotifiers())
return response.JSON(http.StatusOK, notifier.GetAvailableNotifiers())
}
// TODO(codesome): This wont be required in 8.0 since ngalert
// will be enabled by default with no disabling. This is to be removed later.

View File

@@ -59,7 +59,7 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/datasources/edit/*", authorize(reqOrgAdmin, datasources.EditPageAccess), hs.Index)
r.Get("/org/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead)), hs.Index)
r.Get("/org/users/new", reqOrgAdmin, hs.Index)
r.Get("/org/users/invite", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd)), hs.Index)
r.Get("/org/users/invite", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionUsersCreate)), hs.Index)
r.Get("/org/teams", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsRead)), hs.Index)
r.Get("/org/teams/edit/*", authorize(reqCanAccessTeams, teamsEditAccessEvaluator), hs.Index)
r.Get("/org/teams/new", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsCreate)), hs.Index)
@@ -238,9 +238,9 @@ func (hs *HTTPServer) registerRoutes() {
orgRoute.Delete("/users/:userId", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRemove, userIDScope)), routing.Wrap(hs.RemoveOrgUserForCurrentOrg))
// invites
orgRoute.Get("/invites", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd)), routing.Wrap(hs.GetPendingOrgInvites))
orgRoute.Post("/invites", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd)), quota("user"), routing.Wrap(hs.AddOrgInvite))
orgRoute.Patch("/invites/:code/revoke", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd)), routing.Wrap(hs.RevokeInvite))
orgRoute.Get("/invites", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionUsersCreate)), routing.Wrap(hs.GetPendingOrgInvites))
orgRoute.Post("/invites", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionUsersCreate)), quota("user"), routing.Wrap(hs.AddOrgInvite))
orgRoute.Patch("/invites/:code/revoke", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionUsersCreate)), routing.Wrap(hs.RevokeInvite))
// prefs
orgRoute.Get("/preferences", authorize(reqOrgAdmin, ac.EvalPermission(ActionOrgsPreferencesRead)), routing.Wrap(hs.GetOrgPreferences))

View File

@@ -153,17 +153,14 @@ func (hs *HTTPServer) CreateDashboardSnapshot(c *models.ReqContext) response.Res
func (hs *HTTPServer) GetDashboardSnapshot(c *models.ReqContext) response.Response {
key := web.Params(c.Req)[":key"]
if len(key) == 0 {
return response.Error(http.StatusBadRequest, "Empty snapshot key", nil)
return response.Error(404, "Snapshot not found", nil)
}
query := &models.GetDashboardSnapshotQuery{Key: key}
err := hs.DashboardsnapshotsService.GetDashboardSnapshot(c.Req.Context(), query)
if err != nil {
if errors.Is(err, models.ErrDashboardSnapshotNotFound) {
return response.Error(http.StatusNotFound, "Failed to find dashboard snapshot", err)
}
return response.Error(http.StatusInternalServerError, "Failed to get dashboard snapshot", err)
return response.Error(500, "Failed to get dashboard snapshot", err)
}
snapshot := query.Result
@@ -230,10 +227,7 @@ func (hs *HTTPServer) DeleteDashboardSnapshotByDeleteKey(c *models.ReqContext) r
query := &models.GetDashboardSnapshotQuery{DeleteKey: key}
err := hs.DashboardsnapshotsService.GetDashboardSnapshot(c.Req.Context(), query)
if err != nil {
if errors.Is(err, models.ErrDashboardSnapshotNotFound) {
return response.Error(http.StatusNotFound, "Failed to find dashboard snapshot", err)
}
return response.Error(http.StatusInternalServerError, "Failed to get dashboard snapshot", err)
return response.Error(500, "Failed to get dashboard snapshot", err)
}
if query.Result.External {
@@ -266,10 +260,7 @@ func (hs *HTTPServer) DeleteDashboardSnapshot(c *models.ReqContext) response.Res
err := hs.DashboardsnapshotsService.GetDashboardSnapshot(c.Req.Context(), query)
if err != nil {
if errors.Is(err, models.ErrDashboardSnapshotNotFound) {
return response.Error(http.StatusNotFound, "Failed to find dashboard snapshot", err)
}
return response.Error(http.StatusInternalServerError, "Failed to get dashboard snapshot", err)
return response.Error(500, "Failed to get dashboard snapshot", err)
}
if query.Result == nil {
return response.Error(404, "Failed to get dashboard snapshot", nil)

View File

@@ -1,7 +1,6 @@
package api
import (
"errors"
"fmt"
"net/http"
"net/http/httptest"
@@ -230,71 +229,3 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) {
}, sqlmock)
})
}
func TestGetDashboardSnapshotNotFound(t *testing.T) {
sqlmock := mockstore.NewSQLStoreMock()
sqlmock.ExpectedTeamsByUser = []*models.TeamDTO{}
sqlmock.ExpectedError = models.ErrDashboardSnapshotNotFound
hs := &HTTPServer{DashboardsnapshotsService: &dashboardsnapshots.Service{SQLStore: sqlmock}}
loggedInUserScenarioWithRole(t,
"GET /snapshots/{key} should return 404 when the snapshot does not exist", "GET",
"/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) {
sc.handlerFunc = hs.GetDashboardSnapshot
sc.fakeReqWithParams("GET", sc.url, map[string]string{"key": "12345"}).exec()
assert.Equal(t, http.StatusNotFound, sc.resp.Code)
}, sqlmock)
loggedInUserScenarioWithRole(t,
"DELETE /snapshots/{key} should return 404 when the snapshot does not exist", "DELETE",
"/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) {
sc.handlerFunc = hs.DeleteDashboardSnapshot
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec()
assert.Equal(t, http.StatusNotFound, sc.resp.Code)
}, sqlmock)
loggedInUserScenarioWithRole(t,
"GET /snapshots-delete/{deleteKey} should return 404 when the snapshot does not exist", "DELETE",
"/api/snapshots-delete/12345", "/api/snapshots-delete/:deleteKey", models.ROLE_EDITOR, func(sc *scenarioContext) {
sc.handlerFunc = hs.DeleteDashboardSnapshotByDeleteKey
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"deleteKey": "12345"}).exec()
assert.Equal(t, http.StatusNotFound, sc.resp.Code)
}, sqlmock)
}
func TestGetDashboardSnapshotFailure(t *testing.T) {
sqlmock := mockstore.NewSQLStoreMock()
sqlmock.ExpectedTeamsByUser = []*models.TeamDTO{}
sqlmock.ExpectedError = errors.New("something went wrong")
hs := &HTTPServer{DashboardsnapshotsService: &dashboardsnapshots.Service{SQLStore: sqlmock}}
loggedInUserScenarioWithRole(t,
"GET /snapshots/{key} should return 404 when the snapshot does not exist", "GET",
"/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) {
sc.handlerFunc = hs.GetDashboardSnapshot
sc.fakeReqWithParams("GET", sc.url, map[string]string{"key": "12345"}).exec()
assert.Equal(t, http.StatusInternalServerError, sc.resp.Code)
}, sqlmock)
loggedInUserScenarioWithRole(t,
"DELETE /snapshots/{key} should return 404 when the snapshot does not exist", "DELETE",
"/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) {
sc.handlerFunc = hs.DeleteDashboardSnapshot
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec()
assert.Equal(t, http.StatusInternalServerError, sc.resp.Code)
}, sqlmock)
loggedInUserScenarioWithRole(t,
"GET /snapshots-delete/{deleteKey} should return 404 when the snapshot does not exist", "DELETE",
"/api/snapshots-delete/12345", "/api/snapshots-delete/:deleteKey", models.ROLE_EDITOR, func(sc *scenarioContext) {
sc.handlerFunc = hs.DeleteDashboardSnapshotByDeleteKey
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"deleteKey": "12345"}).exec()
assert.Equal(t, http.StatusInternalServerError, sc.resp.Code)
}, sqlmock)
}

View File

@@ -235,8 +235,7 @@ func (hs *HTTPServer) DeleteDataSourceByName(c *models.ReqContext) response.Resp
func validateURL(cmdType string, url string) response.Response {
if _, err := datasource.ValidateURL(cmdType, url); err != nil {
datasourcesLogger.Error("Failed to validate URL", "url", url)
return response.Error(http.StatusBadRequest, "Validation error, invalid URL", err)
return response.Error(400, fmt.Sprintf("Validation error, invalid URL: %q", url), err)
}
return nil
@@ -623,9 +622,13 @@ func (hs *HTTPServer) checkDatasourceHealth(c *models.ReqContext, ds *models.Dat
return response.JSON(http.StatusOK, payload)
}
func (hs *HTTPServer) decryptSecureJsonDataFn(ctx context.Context) func(ds *models.DataSource) (map[string]string, error) {
return func(ds *models.DataSource) (map[string]string, error) {
return hs.DataSourcesService.DecryptedValues(ctx, ds)
func (hs *HTTPServer) decryptSecureJsonDataFn(ctx context.Context) func(ds *models.DataSource) map[string]string {
return func(ds *models.DataSource) map[string]string {
decryptedJsonData, err := hs.DataSourcesService.DecryptedValues(ctx, ds)
if err != nil {
hs.log.Error("Failed to decrypt secure json data", "error", err)
}
return decryptedJsonData
}
}

View File

@@ -30,7 +30,6 @@ import (
//
// Responses:
// 200: snapshotResponse
// 400: badRequestError
// 404: notFoundError
// 500: internalServerError

View File

@@ -2,7 +2,6 @@ package api
import (
"errors"
"fmt"
"net/http"
"github.com/grafana/grafana-plugin-sdk-go/backend"
@@ -24,13 +23,7 @@ func (hs *HTTPServer) handleQueryMetricsError(err error) *response.NormalRespons
if errors.Is(err, models.ErrDataSourceNotFound) {
return response.Error(http.StatusNotFound, "Data source not found", err)
}
var secretsPlugin models.ErrDatasourceSecretsPluginUserFriendly
if errors.As(err, &secretsPlugin) {
return response.Error(http.StatusInternalServerError, fmt.Sprint("Secrets Plugin error: ", err.Error()), err)
}
var badQuery query.ErrBadQuery
var badQuery *query.ErrBadQuery
if errors.As(err, &badQuery) {
return response.Error(http.StatusBadRequest, util.Capitalize(badQuery.Message), err)
}

View File

@@ -1,9 +1,7 @@
package api
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
@@ -39,11 +37,6 @@ type fakePluginRequestValidator struct {
err error
}
type secretsErrorResponseBody struct {
Error string `json:"error"`
Message string `json:"message"`
}
func (rv *fakePluginRequestValidator) Validate(dsURL string, req *http.Request) error {
return rv.err
}
@@ -108,44 +101,3 @@ func TestAPIEndpoint_Metrics_QueryMetricsV2(t *testing.T) {
require.Equal(t, http.StatusMultiStatus, resp.StatusCode)
})
}
func TestAPIEndpoint_Metrics_PluginDecryptionFailure(t *testing.T) {
qds := query.ProvideService(
nil,
nil,
nil,
&fakePluginRequestValidator{},
&fakeDatasources.FakeDataSourceService{SimulatePluginFailure: true},
&fakePluginClient{
QueryDataHandlerFunc: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
resp := backend.Responses{
"A": backend.DataResponse{
Error: fmt.Errorf("query failed"),
},
}
return &backend.QueryDataResponse{Responses: resp}, nil
},
},
&fakeOAuthTokenService{},
)
httpServer := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.queryDataService = qds
})
t.Run("Status code is 500 and a secrets plugin error is returned if there is a problem getting secrets from the remote plugin", func(t *testing.T) {
req := httpServer.NewPostRequest("/api/ds/query", strings.NewReader(queryDatasourceInput))
webtest.RequestWithSignedInUser(req, &models.SignedInUser{UserId: 1, OrgId: 1, OrgRole: models.ROLE_VIEWER})
resp, err := httpServer.SendJSON(req)
require.NoError(t, err)
require.Equal(t, http.StatusInternalServerError, resp.StatusCode)
buf := new(bytes.Buffer)
_, err = buf.ReadFrom(resp.Body)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
var resObj secretsErrorResponseBody
err = json.Unmarshal(buf.Bytes(), &resObj)
require.NoError(t, err)
require.Equal(t, "unknown error", resObj.Error)
require.Contains(t, resObj.Message, "Secrets Plugin error:")
})
}

View File

@@ -5,14 +5,12 @@ import (
"errors"
"fmt"
"net/http"
"strconv"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/events"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/models"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
@@ -40,9 +38,6 @@ func (hs *HTTPServer) AddOrgInvite(c *models.ReqContext) response.Response {
if !inviteDto.Role.IsValid() {
return response.Error(400, "Invalid role specified", nil)
}
if !c.OrgRole.Includes(inviteDto.Role) && !c.IsGrafanaAdmin {
return response.Error(http.StatusForbidden, "Cannot assign a role higher than user's role", nil)
}
// first try get existing user
userQuery := models.GetUserByLoginQuery{LoginOrEmail: inviteDto.LoginOrEmail}
@@ -51,15 +46,6 @@ func (hs *HTTPServer) AddOrgInvite(c *models.ReqContext) response.Response {
return response.Error(500, "Failed to query db for existing user check", err)
}
} else {
// Evaluate permissions for adding an existing user to the organization
userIDScope := ac.Scope("users", "id", strconv.Itoa(int(userQuery.Result.Id)))
hasAccess, err := hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, ac.EvalPermission(ac.ActionOrgUsersAdd, userIDScope))
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to evaluate permissions", err)
}
if !hasAccess {
return response.Error(http.StatusForbidden, "Permission denied: not permitted to add an existing user to this organisation", err)
}
return hs.inviteExistingUserToOrg(c, userQuery.Result, &inviteDto)
}

View File

@@ -1,78 +0,0 @@
package api
import (
"net/http"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
)
func TestOrgInvitesAPIEndpointAccess(t *testing.T) {
type accessControlTestCase2 struct {
expectedCode int
desc string
url string
method string
permissions []*accesscontrol.Permission
input string
}
tests := []accessControlTestCase2{
{
expectedCode: http.StatusOK,
desc: "org viewer with the correct permissions can invite an existing user to his org",
url: "/api/org/invites",
method: http.MethodPost,
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersAdd, Scope: accesscontrol.ScopeUsersAll}},
input: `{"loginOrEmail": "` + testAdminOrg2.Login + `", "role": "` + string(models.ROLE_VIEWER) + `"}`,
},
{
expectedCode: http.StatusForbidden,
desc: "org viewer with missing permissions cannot invite an existing user to his org",
url: "/api/org/invites",
method: http.MethodPost,
permissions: []*accesscontrol.Permission{},
input: `{"loginOrEmail": "` + testAdminOrg2.Login + `", "role": "` + string(models.ROLE_VIEWER) + `"}`,
},
{
expectedCode: http.StatusForbidden,
desc: "org viewer with the wrong scope cannot invite an existing user to his org",
url: "/api/org/invites",
method: http.MethodPost,
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersAdd, Scope: "users:id:100"}},
input: `{"loginOrEmail": "` + testAdminOrg2.Login + `", "role": "` + string(models.ROLE_VIEWER) + `"}`,
},
{
expectedCode: http.StatusOK,
desc: "org viewer with the correct permissions can invite a new user to his org",
url: "/api/org/invites",
method: http.MethodPost,
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersAdd, Scope: accesscontrol.ScopeUsersAll}},
input: `{"loginOrEmail": "new user", "role": "` + string(models.ROLE_VIEWER) + `"}`,
},
{
expectedCode: http.StatusForbidden,
desc: "org viewer with missing permissions cannot invite a new user to his org",
url: "/api/org/invites",
method: http.MethodPost,
permissions: []*accesscontrol.Permission{},
input: `{"loginOrEmail": "new user", "role": "` + string(models.ROLE_VIEWER) + `"}`,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
sc := setupHTTPServer(t, true, true)
setInitCtxSignedInViewer(sc.initCtx)
setupOrgUsersDBForAccessControlTests(t, sc.db)
setAccessControlPermissions(sc.acmock, test.permissions, sc.initCtx.OrgId)
input := strings.NewReader(test.input)
response := callAPI(sc.server, test.method, test.url, input, t)
assert.Equal(t, test.expectedCode, response.Code)
})
}
}

View File

@@ -21,7 +21,7 @@ func (hs *HTTPServer) AddOrgUserToCurrentOrg(c *models.ReqContext) response.Resp
return response.Error(http.StatusBadRequest, "bad request data", err)
}
cmd.OrgId = c.OrgId
return hs.addOrgUserHelper(c, cmd)
return hs.addOrgUserHelper(c.Req.Context(), cmd)
}
// POST /api/orgs/:orgId/users
@@ -36,19 +36,16 @@ func (hs *HTTPServer) AddOrgUser(c *models.ReqContext) response.Response {
if err != nil {
return response.Error(http.StatusBadRequest, "orgId is invalid", err)
}
return hs.addOrgUserHelper(c, cmd)
return hs.addOrgUserHelper(c.Req.Context(), cmd)
}
func (hs *HTTPServer) addOrgUserHelper(c *models.ReqContext, cmd models.AddOrgUserCommand) response.Response {
func (hs *HTTPServer) addOrgUserHelper(ctx context.Context, cmd models.AddOrgUserCommand) response.Response {
if !cmd.Role.IsValid() {
return response.Error(400, "Invalid role specified", nil)
}
if !c.OrgRole.Includes(cmd.Role) && !c.IsGrafanaAdmin {
return response.Error(http.StatusForbidden, "Cannot assign a role higher than user's role", nil)
}
userQuery := models.GetUserByLoginQuery{LoginOrEmail: cmd.LoginOrEmail}
err := hs.SQLStore.GetUserByLogin(c.Req.Context(), &userQuery)
err := hs.SQLStore.GetUserByLogin(ctx, &userQuery)
if err != nil {
return response.Error(404, "User not found", nil)
}
@@ -57,7 +54,7 @@ func (hs *HTTPServer) addOrgUserHelper(c *models.ReqContext, cmd models.AddOrgUs
cmd.UserId = userToAdd.Id
if err := hs.SQLStore.AddOrgUser(c.Req.Context(), &cmd); err != nil {
if err := hs.SQLStore.AddOrgUser(ctx, &cmd); err != nil {
if errors.Is(err, models.ErrOrgUserAlreadyAdded) {
return response.JSON(409, util.DynMap{
"message": "User is already member of this organization",
@@ -220,7 +217,7 @@ func (hs *HTTPServer) UpdateOrgUserForCurrentOrg(c *models.ReqContext) response.
if err != nil {
return response.Error(http.StatusBadRequest, "userId is invalid", err)
}
return hs.updateOrgUserHelper(c, cmd)
return hs.updateOrgUserHelper(c.Req.Context(), cmd)
}
// PATCH /api/orgs/:orgId/users/:userId
@@ -238,17 +235,14 @@ func (hs *HTTPServer) UpdateOrgUser(c *models.ReqContext) response.Response {
if err != nil {
return response.Error(http.StatusBadRequest, "userId is invalid", err)
}
return hs.updateOrgUserHelper(c, cmd)
return hs.updateOrgUserHelper(c.Req.Context(), cmd)
}
func (hs *HTTPServer) updateOrgUserHelper(c *models.ReqContext, cmd models.UpdateOrgUserCommand) response.Response {
func (hs *HTTPServer) updateOrgUserHelper(ctx context.Context, cmd models.UpdateOrgUserCommand) response.Response {
if !cmd.Role.IsValid() {
return response.Error(400, "Invalid role specified", nil)
}
if !c.OrgRole.Includes(cmd.Role) && !c.IsGrafanaAdmin {
return response.Error(http.StatusForbidden, "Cannot assign a role higher than user's role", nil)
}
if err := hs.SQLStore.UpdateOrgUser(c.Req.Context(), &cmd); err != nil {
if err := hs.SQLStore.UpdateOrgUser(ctx, &cmd); err != nil {
if errors.Is(err, models.ErrLastOrgAdmin) {
return response.Error(400, "Cannot change role so that there is no organization admin left", nil)
}

View File

@@ -570,112 +570,6 @@ func TestPostOrgUsersAPIEndpoint_AccessControl(t *testing.T) {
}
}
func TestOrgUsersAPIEndpointWithSetPerms_AccessControl(t *testing.T) {
type accessControlTestCase2 struct {
expectedCode int
desc string
url string
method string
permissions []*accesscontrol.Permission
input string
}
tests := []accessControlTestCase2{
{
expectedCode: http.StatusOK,
desc: "org viewer with the correct permissions can add a user as a viewer to his org",
url: "/api/org/users",
method: http.MethodPost,
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersAdd, Scope: accesscontrol.ScopeUsersAll}},
input: `{"loginOrEmail": "` + testAdminOrg2.Login + `", "role": "` + string(models.ROLE_VIEWER) + `"}`,
},
{
expectedCode: http.StatusForbidden,
desc: "org viewer with the correct permissions cannot add a user as an editor to his org",
url: "/api/org/users",
method: http.MethodPost,
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersAdd, Scope: accesscontrol.ScopeUsersAll}},
input: `{"loginOrEmail": "` + testAdminOrg2.Login + `", "role": "` + string(models.ROLE_EDITOR) + `"}`,
},
{
expectedCode: http.StatusOK,
desc: "org viewer with the correct permissions can add a user as a viewer to his org",
url: "/api/orgs/1/users",
method: http.MethodPost,
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersAdd, Scope: accesscontrol.ScopeUsersAll}},
input: `{"loginOrEmail": "` + testAdminOrg2.Login + `", "role": "` + string(models.ROLE_VIEWER) + `"}`,
},
{
expectedCode: http.StatusForbidden,
desc: "org viewer with the correct permissions cannot add a user as an editor to his org",
url: "/api/orgs/1/users",
method: http.MethodPost,
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersAdd, Scope: accesscontrol.ScopeUsersAll}},
input: `{"loginOrEmail": "` + testAdminOrg2.Login + `", "role": "` + string(models.ROLE_EDITOR) + `"}`,
},
{
expectedCode: http.StatusOK,
desc: "org viewer with the correct permissions can update a user's role to a viewer in his org",
url: fmt.Sprintf("/api/org/users/%d", testEditorOrg1.UserId),
method: http.MethodPatch,
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersWrite, Scope: accesscontrol.ScopeUsersAll}},
input: `{"role": "` + string(models.ROLE_VIEWER) + `"}`,
},
{
expectedCode: http.StatusForbidden,
desc: "org viewer with the correct permissions cannot update a user's role to a viewer in his org",
url: fmt.Sprintf("/api/org/users/%d", testEditorOrg1.UserId),
method: http.MethodPatch,
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersWrite, Scope: accesscontrol.ScopeUsersAll}},
input: `{"role": "` + string(models.ROLE_EDITOR) + `"}`,
},
{
expectedCode: http.StatusOK,
desc: "org viewer with the correct permissions can update a user's role to a viewer in his org",
url: fmt.Sprintf("/api/orgs/1/users/%d", testEditorOrg1.UserId),
method: http.MethodPatch,
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersWrite, Scope: accesscontrol.ScopeUsersAll}},
input: `{"role": "` + string(models.ROLE_VIEWER) + `"}`,
},
{
expectedCode: http.StatusForbidden,
desc: "org viewer with the correct permissions cannot update a user's role to a viewer in his org",
url: fmt.Sprintf("/api/orgs/1/users/%d", testEditorOrg1.UserId),
method: http.MethodPatch,
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersWrite, Scope: accesscontrol.ScopeUsersAll}},
input: `{"role": "` + string(models.ROLE_EDITOR) + `"}`,
},
{
expectedCode: http.StatusOK,
desc: "org viewer with the correct permissions can invite a user as a viewer in his org",
url: "/api/org/invites",
method: http.MethodPost,
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersAdd, Scope: accesscontrol.ScopeUsersAll}},
input: `{"loginOrEmail": "newUserEmail@test.com", "sendEmail": false, "role": "` + string(models.ROLE_VIEWER) + `"}`,
},
{
expectedCode: http.StatusForbidden,
desc: "org viewer with the correct permissions cannot invite a user as an editor in his org",
url: "/api/org/invites",
method: http.MethodPost,
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionUsersCreate}},
input: `{"loginOrEmail": "newUserEmail@test.com", "sendEmail": false, "role": "` + string(models.ROLE_EDITOR) + `"}`,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
sc := setupHTTPServer(t, true, true)
setInitCtxSignedInViewer(sc.initCtx)
setupOrgUsersDBForAccessControlTests(t, sc.db)
setAccessControlPermissions(sc.acmock, test.permissions, sc.initCtx.OrgId)
input := strings.NewReader(test.input)
response := callAPI(sc.server, test.method, test.url, input, t)
assert.Equal(t, test.expectedCode, response.Code)
})
}
}
func TestPatchOrgUsersAPIEndpoint_AccessControl(t *testing.T) {
url := "/api/orgs/%v/users/%v"
type testCase struct {

View File

@@ -126,8 +126,12 @@ func hiddenRefIDs(queries []Query) (map[string]struct{}, error) {
return hidden, nil
}
func (s *Service) decryptSecureJsonDataFn(ctx context.Context) func(ds *models.DataSource) (map[string]string, error) {
return func(ds *models.DataSource) (map[string]string, error) {
return s.dataSourceService.DecryptedValues(ctx, ds)
func (s *Service) decryptSecureJsonDataFn(ctx context.Context) func(ds *models.DataSource) map[string]string {
return func(ds *models.DataSource) map[string]string {
decryptedJsonData, err := s.dataSourceService.DecryptedValues(ctx, ds)
if err != nil {
logger.Error("Failed to decrypt secure json data", "error", err)
}
return decryptedJsonData
}
}

View File

@@ -81,15 +81,6 @@ func (ds DataSource) AllowedCookies() []string {
return []string{}
}
// Specific error type for grpc secrets management so that we can show more detailed plugin errors to users
type ErrDatasourceSecretsPluginUserFriendly struct {
Err string
}
func (e ErrDatasourceSecretsPluginUserFriendly) Error() string {
return e.Err
}
// ----------------------
// COMMANDS

View File

@@ -3,27 +3,22 @@ package adapters
import (
"encoding/json"
"fmt"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/models"
)
// ModelToInstanceSettings converts a datasources.DataSource to a backend.DataSourceInstanceSettings.
func ModelToInstanceSettings(ds *models.DataSource, decryptFn func(ds *models.DataSource) (map[string]string, error),
// ModelToInstanceSettings converts a models.DataSource to a backend.DataSourceInstanceSettings.
func ModelToInstanceSettings(ds *models.DataSource, decryptFn func(ds *models.DataSource) map[string]string,
) (*backend.DataSourceInstanceSettings, error) {
var jsonDataBytes json.RawMessage
if ds.JsonData != nil {
var err error
jsonDataBytes, err = ds.JsonData.MarshalJSON()
if err != nil {
return nil, fmt.Errorf("failed to convert data source to instance settings: %w", err)
return nil, err
}
}
decrypted, err := decryptFn(ds)
if err != nil {
return nil, err
}
return &backend.DataSourceInstanceSettings{
ID: ds.Id,
@@ -35,9 +30,9 @@ func ModelToInstanceSettings(ds *models.DataSource, decryptFn func(ds *models.Da
BasicAuthEnabled: ds.BasicAuth,
BasicAuthUser: ds.BasicAuthUser,
JSONData: jsonDataBytes,
DecryptedSecureJSONData: decrypted,
DecryptedSecureJSONData: decryptFn(ds),
Updated: ds.Updated,
}, err
}, nil
}
// BackendUserFromSignedInUser converts Grafana's SignedInUser model

View File

@@ -15,8 +15,6 @@ import (
"path/filepath"
"strings"
"github.com/gobwas/glob"
// TODO: replace deprecated `golang.org/x/crypto` package https://github.com/grafana/grafana/issues/46050
// nolint:staticcheck
"golang.org/x/crypto/openpgp"
@@ -26,6 +24,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util/errutil"
)
// Soon we can fetch keys from:
@@ -86,11 +85,18 @@ func readPluginManifest(body []byte) (*pluginManifest, error) {
var manifest pluginManifest
err := json.Unmarshal(block.Plaintext, &manifest)
if err != nil {
return nil, fmt.Errorf("%v: %w", "Error parsing manifest JSON", err)
return nil, errutil.Wrap("Error parsing manifest JSON", err)
}
if err = validateManifest(manifest, block); err != nil {
return nil, err
keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewBufferString(publicKeyText))
if err != nil {
return nil, errutil.Wrap("failed to parse public key", err)
}
if _, err := openpgp.CheckDetachedSignature(keyring,
bytes.NewBuffer(block.Bytes),
block.ArmoredSignature.Body); err != nil {
return nil, errutil.Wrap("failed to check signature", err)
}
return &manifest, nil
@@ -126,7 +132,7 @@ func Calculate(mlog log.Logger, plugin *plugins.Plugin) (plugins.Signature, erro
manifest, err := readPluginManifest(byteValue)
if err != nil {
mlog.Debug("Plugin signature invalid", "id", plugin.ID, "err", err)
mlog.Debug("Plugin signature invalid", "id", plugin.ID)
return plugins.Signature{
Status: plugins.SignatureInvalid,
}, nil
@@ -139,14 +145,32 @@ func Calculate(mlog log.Logger, plugin *plugins.Plugin) (plugins.Signature, erro
}, nil
}
// Validate that plugin is running within defined root URLs
if len(manifest.RootURLs) > 0 {
if match, err := urlMatch(manifest.RootURLs, setting.AppUrl, manifest.SignatureType); err != nil {
mlog.Warn("Could not verify if root URLs match", "plugin", plugin.ID, "rootUrls", manifest.RootURLs)
// Validate that private is running within defined root URLs
if manifest.SignatureType == plugins.PrivateSignature {
appURL, err := url.Parse(setting.AppUrl)
if err != nil {
return plugins.Signature{}, err
} else if !match {
}
foundMatch := false
for _, u := range manifest.RootURLs {
rootURL, err := url.Parse(u)
if err != nil {
mlog.Warn("Could not parse plugin root URL", "plugin", plugin.ID, "rootUrl", rootURL)
return plugins.Signature{}, err
}
if rootURL.Scheme == appURL.Scheme &&
rootURL.Host == appURL.Host &&
path.Clean(rootURL.RequestURI()) == path.Clean(appURL.RequestURI()) {
foundMatch = true
break
}
}
if !foundMatch {
mlog.Warn("Could not find root URL that matches running application URL", "plugin", plugin.ID,
"appUrl", setting.AppUrl, "rootUrls", manifest.RootURLs)
"appUrl", appURL, "rootUrls", manifest.RootURLs)
return plugins.Signature{
Status: plugins.SignatureInvalid,
}, nil
@@ -276,84 +300,3 @@ func pluginFilesRequiringVerification(plugin *plugins.Plugin) ([]string, error)
return files, err
}
func urlMatch(specs []string, target string, signatureType plugins.SignatureType) (bool, error) {
targetURL, err := url.Parse(target)
if err != nil {
return false, err
}
for _, spec := range specs {
specURL, err := url.Parse(spec)
if err != nil {
return false, err
}
if specURL.Scheme == targetURL.Scheme && specURL.Host == targetURL.Host &&
path.Clean(specURL.RequestURI()) == path.Clean(targetURL.RequestURI()) {
return true, nil
}
if signatureType != plugins.PrivateGlobSignature {
continue
}
sp, err := glob.Compile(spec, '/', '.')
if err != nil {
return false, err
}
if match := sp.Match(target); match {
return true, nil
}
}
return false, nil
}
type invalidFieldErr struct {
field string
}
func (r invalidFieldErr) Error() string {
return fmt.Sprintf("valid manifest field %s is required", r.field)
}
func validateManifest(m pluginManifest, block *clearsign.Block) error {
if len(m.Plugin) == 0 {
return invalidFieldErr{field: "plugin"}
}
if len(m.Version) == 0 {
return invalidFieldErr{field: "version"}
}
if len(m.KeyID) == 0 {
return invalidFieldErr{field: "keyId"}
}
if m.Time == 0 {
return invalidFieldErr{field: "time"}
}
if len(m.Files) == 0 {
return invalidFieldErr{field: "files"}
}
if m.isV2() {
if len(m.SignedByOrg) == 0 {
return invalidFieldErr{field: "signedByOrg"}
}
if len(m.SignedByOrgName) == 0 {
return invalidFieldErr{field: "signedByOrgName"}
}
if !m.SignatureType.IsValid() {
return fmt.Errorf("%s is not a valid signature type", m.SignatureType)
}
}
keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewBufferString(publicKeyText))
if err != nil {
return fmt.Errorf("%v: %w", "failed to parse public key", err)
}
if _, err = openpgp.CheckDetachedSignature(keyring,
bytes.NewBuffer(block.Bytes),
block.ArmoredSignature.Body); err != nil {
return fmt.Errorf("%v: %w", "failed to check signature", err)
}
return nil
}

View File

@@ -121,359 +121,3 @@ func fileList(manifest *pluginManifest) []string {
sort.Strings(keys)
return keys
}
func Test_urlMatch_privateGlob(t *testing.T) {
type args struct {
specs []string
target string
}
tests := []struct {
name string
args args
shouldMatch bool
}{
{
name: "Support single wildcard matching single subdomain",
args: args{
specs: []string{"https://*.example.com"},
target: "https://test.example.com",
},
shouldMatch: true,
},
{
name: "Do not support single wildcard matching multiple subdomains",
args: args{
specs: []string{"https://*.example.com"},
target: "https://more.test.example.com",
},
shouldMatch: false,
},
{
name: "Support multiple wildcards matching multiple subdomains",
args: args{
specs: []string{"https://**.example.com"},
target: "https://test.example.com",
},
shouldMatch: true,
},
{
name: "Support multiple wildcards matching multiple subdomains",
args: args{
specs: []string{"https://**.example.com"},
target: "https://more.test.example.com",
},
shouldMatch: true,
},
{
name: "Support single wildcard matching single paths",
args: args{
specs: []string{"https://www.example.com/*"},
target: "https://www.example.com/grafana1",
},
shouldMatch: true,
},
{
name: "Do not support single wildcard matching multiple paths",
args: args{
specs: []string{"https://www.example.com/*"},
target: "https://www.example.com/other/grafana",
},
shouldMatch: false,
},
{
name: "Support double wildcard matching multiple paths",
args: args{
specs: []string{"https://www.example.com/**"},
target: "https://www.example.com/other/grafana",
},
shouldMatch: true,
},
{
name: "Do not support subdomain mismatch",
args: args{
specs: []string{"https://www.test.example.com/grafana/docs"},
target: "https://www.dev.example.com/grafana/docs",
},
shouldMatch: false,
},
{
name: "Support single wildcard matching single path",
args: args{
specs: []string{"https://www.example.com/grafana*"},
target: "https://www.example.com/grafana1",
},
shouldMatch: true,
},
{
name: "Do not support single wildcard matching different path prefix",
args: args{
specs: []string{"https://www.example.com/grafana*"},
target: "https://www.example.com/somethingelse",
},
shouldMatch: false,
},
{
name: "Do not support path mismatch",
args: args{
specs: []string{"https://example.com/grafana"},
target: "https://example.com/grafana1",
},
shouldMatch: false,
},
{
name: "Support both domain and path wildcards",
args: args{
specs: []string{"https://*.example.com/*"},
target: "https://www.example.com/grafana1",
},
shouldMatch: true,
},
{
name: "Do not support wildcards without TLDs",
args: args{
specs: []string{"https://example.*"},
target: "https://www.example.com/grafana1",
},
shouldMatch: false,
},
{
name: "Support exact match",
args: args{
specs: []string{"https://example.com/test"},
target: "https://example.com/test",
},
shouldMatch: true,
},
{
name: "Does not support scheme mismatch",
args: args{
specs: []string{"https://test.example.com/grafana"},
target: "http://test.example.com/grafana",
},
shouldMatch: false,
},
{
name: "Support trailing slash in spec",
args: args{
specs: []string{"https://example.com/"},
target: "https://example.com",
},
shouldMatch: true,
},
{
name: "Support trailing slash in target",
args: args{
specs: []string{"https://example.com"},
target: "https://example.com/",
},
shouldMatch: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := urlMatch(tt.args.specs, tt.args.target, plugins.PrivateGlobSignature)
require.NoError(t, err)
require.Equal(t, tt.shouldMatch, got)
})
}
}
func Test_urlMatch_private(t *testing.T) {
type args struct {
specs []string
target string
}
tests := []struct {
name string
args args
shouldMatch bool
}{
{
name: "Support exact match",
args: args{
specs: []string{"https://example.com/test"},
target: "https://example.com/test",
},
shouldMatch: true,
},
{
name: "Support trailing slash in spec",
args: args{
specs: []string{"https://example.com/test/"},
target: "https://example.com/test",
},
shouldMatch: true,
},
{
name: "Support trailing slash in target",
args: args{
specs: []string{"https://example.com/test"},
target: "https://example.com/test/",
},
shouldMatch: true,
},
{
name: "Do not support single wildcard matching single subdomain",
args: args{
specs: []string{"https://*.example.com"},
target: "https://test.example.com",
},
shouldMatch: false,
},
{
name: "Do not support multiple wildcards matching multiple subdomains",
args: args{
specs: []string{"https://**.example.com"},
target: "https://more.test.example.com",
},
shouldMatch: false,
},
{
name: "Do not support single wildcard matching single paths",
args: args{
specs: []string{"https://www.example.com/*"},
target: "https://www.example.com/grafana1",
},
shouldMatch: false,
},
{
name: "Do not support double wildcard matching multiple paths",
args: args{
specs: []string{"https://www.example.com/**"},
target: "https://www.example.com/other/grafana",
},
shouldMatch: false,
},
{
name: "Do not support subdomain mismatch",
args: args{
specs: []string{"https://www.test.example.com/grafana/docs"},
target: "https://www.dev.example.com/grafana/docs",
},
shouldMatch: false,
},
{
name: "Do not support path mismatch",
args: args{
specs: []string{"https://example.com/grafana"},
target: "https://example.com/grafana1",
},
shouldMatch: false,
},
{
name: "Do not support both domain and path wildcards",
args: args{
specs: []string{"https://*.example.com/*"},
target: "https://www.example.com/grafana1",
},
shouldMatch: false,
},
{
name: "Do not support wildcards without TLDs",
args: args{
specs: []string{"https://example.*"},
target: "https://www.example.com/grafana1",
},
shouldMatch: false,
},
{
name: "Do not support scheme mismatch",
args: args{
specs: []string{"https://test.example.com/grafana"},
target: "http://test.example.com/grafana",
},
shouldMatch: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := urlMatch(tt.args.specs, tt.args.target, plugins.PrivateSignature)
require.NoError(t, err)
require.Equal(t, tt.shouldMatch, got)
})
}
}
func Test_validateManifest(t *testing.T) {
tcs := []struct {
name string
manifest *pluginManifest
expectedErr string
}{
{
name: "Empty plugin field",
manifest: createV2Manifest(t, func(m *pluginManifest) { m.Plugin = "" }),
expectedErr: "valid manifest field plugin is required",
},
{
name: "Empty keyId field",
manifest: createV2Manifest(t, func(m *pluginManifest) { m.KeyID = "" }),
expectedErr: "valid manifest field keyId is required",
},
{
name: "Empty signedByOrg field",
manifest: createV2Manifest(t, func(m *pluginManifest) { m.SignedByOrg = "" }),
expectedErr: "valid manifest field signedByOrg is required",
},
{
name: "Empty signedByOrgName field",
manifest: createV2Manifest(t, func(m *pluginManifest) { m.SignedByOrgName = "" }),
expectedErr: "valid manifest field SignedByOrgName is required",
},
{
name: "Empty signatureType field",
manifest: createV2Manifest(t, func(m *pluginManifest) { m.SignatureType = "" }),
expectedErr: "valid manifest field signatureType is required",
},
{
name: "Invalid signatureType field",
manifest: createV2Manifest(t, func(m *pluginManifest) { m.SignatureType = "invalidSignatureType" }),
expectedErr: "valid manifest field signatureType is required",
},
{
name: "Empty files field",
manifest: createV2Manifest(t, func(m *pluginManifest) { m.Files = map[string]string{} }),
expectedErr: "valid manifest field files is required",
},
{
name: "Empty time field",
manifest: createV2Manifest(t, func(m *pluginManifest) { m.Time = 0 }),
expectedErr: "valid manifest field time is required",
},
{
name: "Empty version field",
manifest: createV2Manifest(t, func(m *pluginManifest) { m.Version = "" }),
expectedErr: "valid manifest field version is required",
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
err := validateManifest(*tc.manifest, nil)
require.Errorf(t, err, tc.expectedErr)
})
}
}
func createV2Manifest(t *testing.T, cbs ...func(*pluginManifest)) *pluginManifest {
t.Helper()
m := &pluginManifest{
Plugin: "grafana-test-app",
Version: "2.5.3",
KeyID: "7e4d0c6a708866e7",
Time: 1586817677115,
Files: map[string]string{
"plugin.json": "55556b845e91935cc48fae3aa67baf0f22694c3f",
},
ManifestVersion: "2.0.0",
SignatureType: plugins.GrafanaSignature,
SignedByOrg: "grafana",
SignedByOrgName: "grafana",
}
for _, cb := range cbs {
cb(m)
}
return m
}

View File

@@ -176,21 +176,10 @@ const (
type SignatureType string
const (
GrafanaSignature SignatureType = "grafana"
CommercialSignature SignatureType = "commercial"
CommunitySignature SignatureType = "community"
PrivateSignature SignatureType = "private"
PrivateGlobSignature SignatureType = "private-glob"
GrafanaSignature SignatureType = "grafana"
PrivateSignature SignatureType = "private"
)
func (s SignatureType) IsValid() bool {
switch s {
case GrafanaSignature, CommercialSignature, CommunitySignature, PrivateSignature, PrivateGlobSignature:
return true
}
return false
}
type PluginFiles map[string]struct{}
type Signature struct {

View File

@@ -127,8 +127,12 @@ func (p *Provider) getCachedPluginSettings(ctx context.Context, pluginID string,
return ps, nil
}
func (p *Provider) decryptSecureJsonDataFn(ctx context.Context) func(ds *models.DataSource) (map[string]string, error) {
return func(ds *models.DataSource) (map[string]string, error) {
return p.dataSourceService.DecryptedValues(ctx, ds)
func (p *Provider) decryptSecureJsonDataFn(ctx context.Context) func(ds *models.DataSource) map[string]string {
return func(ds *models.DataSource) map[string]string {
decryptedJsonData, err := p.dataSourceService.DecryptedValues(ctx, ds)
if err != nil {
p.logger.Error("Failed to decrypt secure json data", "error", err)
}
return decryptedJsonData
}
}

View File

@@ -16,7 +16,6 @@ import (
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/live"
"github.com/grafana/grafana/pkg/services/live/pushhttp"
"github.com/grafana/grafana/pkg/services/login/authinfoservice"
"github.com/grafana/grafana/pkg/services/ngalert"
"github.com/grafana/grafana/pkg/services/notifications"
plugindashboardsservice "github.com/grafana/grafana/pkg/services/plugindashboards/service"
@@ -39,7 +38,6 @@ func ProvideBackgroundServiceRegistry(
pluginsUpdateChecker *updatechecker.PluginsService, metrics *metrics.InternalMetricsService,
secretsService *secretsManager.SecretsService, remoteCache *remotecache.RemoteCache,
thumbnailsService thumbs.Service, StorageService store.StorageService, searchService searchV2.SearchService, entityEventsService store.EntityEventsService,
authInfoService *authinfoservice.Implementation,
// Need to make sure these are initialized, is there a better place to put them?
_ *dashboardsnapshots.Service, _ *alerting.AlertNotificationService,
_ serviceaccounts.Service, _ *guardian.Provider,
@@ -69,7 +67,6 @@ func ProvideBackgroundServiceRegistry(
thumbnailsService,
searchService,
entityEventsService,
authInfoService,
)
}

View File

@@ -489,7 +489,7 @@ func (dr *DashboardServiceImpl) setDefaultPermissions(ctx context.Context, dto *
inFolder := dash.FolderId > 0
if !accesscontrol.IsDisabled(dr.cfg) {
var permissions []accesscontrol.SetResourcePermissionCommand
if !provisioned && dto.User.IsRealUser() && !dto.User.IsAnonymous {
if !provisioned {
permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{
UserID: dto.User.UserId, Permission: models.PERMISSION_ADMIN.String(),
})
@@ -511,7 +511,7 @@ func (dr *DashboardServiceImpl) setDefaultPermissions(ctx context.Context, dto *
if err != nil {
return err
}
} else if dr.cfg.EditorsCanAdmin && !provisioned && dto.User.IsRealUser() && !dto.User.IsAnonymous {
} else if dr.cfg.EditorsCanAdmin && !provisioned {
if err := dr.MakeUserAdmin(ctx, dto.OrgId, dto.User.UserId, dash.Id, !inFolder); err != nil {
return err
}

View File

@@ -169,20 +169,12 @@ func (f *FolderServiceImpl) CreateFolder(ctx context.Context, user *models.Signe
var permissionErr error
if !accesscontrol.IsDisabled(f.cfg) {
var permissions []accesscontrol.SetResourcePermissionCommand
if user.IsRealUser() && !user.IsAnonymous {
permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{
UserID: userID, Permission: models.PERMISSION_ADMIN.String(),
})
}
permissions = append(permissions, []accesscontrol.SetResourcePermissionCommand{
_, permissionErr = f.permissions.SetPermissions(ctx, orgID, folder.Uid, []accesscontrol.SetResourcePermissionCommand{
{UserID: userID, Permission: models.PERMISSION_ADMIN.String()},
{BuiltinRole: string(models.ROLE_EDITOR), Permission: models.PERMISSION_EDIT.String()},
{BuiltinRole: string(models.ROLE_VIEWER), Permission: models.PERMISSION_VIEW.String()},
}...)
_, permissionErr = f.permissions.SetPermissions(ctx, orgID, folder.Uid, permissions...)
} else if f.cfg.EditorsCanAdmin && user.IsRealUser() && !user.IsAnonymous {
} else if f.cfg.EditorsCanAdmin {
permissionErr = f.MakeUserAdmin(ctx, orgID, userID, folder.Id, true)
}

View File

@@ -11,9 +11,8 @@ import (
)
type FakeDataSourceService struct {
lastId int64
DataSources []*models.DataSource
SimulatePluginFailure bool
lastId int64
DataSources []*models.DataSource
}
var _ datasources.DataSourceService = &FakeDataSourceService{}
@@ -108,9 +107,6 @@ func (s *FakeDataSourceService) GetHTTPTransport(ctx context.Context, ds *models
}
func (s *FakeDataSourceService) DecryptedValues(ctx context.Context, ds *models.DataSource) (map[string]string, error) {
if s.SimulatePluginFailure {
return nil, models.ErrDatasourceSecretsPluginUserFriendly{Err: "unknown error"}
}
values := make(map[string]string)
return values, nil
}

View File

@@ -27,12 +27,7 @@ SELECT DISTINCT
, (SELECT COUNT(connection_id) FROM ` + models.LibraryElementConnectionTableName + ` WHERE element_id = le.id AND kind=1) AS connected_dashboards`
)
const deleteInvalidConnections = `
DELETE FROM library_element_connection
WHERE connection_id IN (
SELECT connection_id as id FROM library_element_connection
WHERE element_id=? AND connection_id NOT IN (SELECT id as connection_id from dashboard)
)`
const deleteInvalidConnections = "DELETE FROM library_element_connection WHERE connection_id IN (SELECT connection_id as id FROM library_element_connection WHERE element_id=? EXCEPT SELECT id from dashboard)"
func getFromLibraryElementDTOWithMeta(dialect migrator.Dialect) string {
user := dialect.Quote("user")
@@ -594,7 +589,7 @@ func (l *LibraryElementService) getConnections(c context.Context, signedInUser *
return connections, err
}
// getElementsForDashboardID gets all elements for a specific dashboard
//getElementsForDashboardID gets all elements for a specific dashboard
func (l *LibraryElementService) getElementsForDashboardID(c context.Context, dashboardID int64) (map[string]LibraryElementDTO, error) {
libraryElementMap := make(map[string]LibraryElementDTO)
err := l.SQLStore.WithDbSession(c, func(session *sqlstore.DBSession) error {

View File

@@ -25,7 +25,6 @@ func ProvideAuthInfoStore(sqlStore sqlstore.Store, secretsService secrets.Servic
secretsService: secretsService,
logger: log.New("login.authinfo.store"),
}
InitMetrics()
return store
}

View File

@@ -1,149 +0,0 @@
package database
import (
"context"
"sync"
"time"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/prometheus/client_golang/prometheus"
)
type LoginStats struct {
DuplicateUserEntries int `xorm:"duplicate_user_entries"`
MixedCasedUsers int `xorm:"mixed_cased_users"`
}
const (
ExporterName = "grafana"
metricsCollectionInterval = time.Second * 60 * 4 // every 4 hours, indication of duplicate users
)
var (
// MStatDuplicateUserEntries is a indication metric gauge for number of users with duplicate emails or logins
MStatDuplicateUserEntries prometheus.Gauge
// MStatHasDuplicateEntries is a metric for if there is duplicate users
MStatHasDuplicateEntries prometheus.Gauge
// MStatMixedCasedUsers is a metric for if there is duplicate users
MStatMixedCasedUsers prometheus.Gauge
once sync.Once
Initialised bool = false
)
func InitMetrics() {
once.Do(func() {
MStatDuplicateUserEntries = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "stat_users_total_duplicate_user_entries",
Help: "total number of duplicate user entries by email or login",
Namespace: ExporterName,
})
MStatHasDuplicateEntries = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "stat_users_has_duplicate_user_entries",
Help: "instance has duplicate user entries by email or login",
Namespace: ExporterName,
})
MStatMixedCasedUsers = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "stat_users_total_mixed_cased_users",
Help: "total number of users with upper and lower case logins or emails",
Namespace: ExporterName,
})
prometheus.MustRegister(
MStatDuplicateUserEntries,
MStatHasDuplicateEntries,
MStatMixedCasedUsers,
)
})
}
func (s *AuthInfoStore) RunMetricsCollection(ctx context.Context) error {
if _, err := s.GetLoginStats(ctx); err != nil {
s.logger.Warn("Failed to get authinfo metrics", "error", err.Error())
}
updateStatsTicker := time.NewTicker(metricsCollectionInterval)
defer updateStatsTicker.Stop()
for {
select {
case <-updateStatsTicker.C:
if _, err := s.GetLoginStats(ctx); err != nil {
s.logger.Warn("Failed to get authinfo metrics", "error", err.Error())
}
case <-ctx.Done():
return ctx.Err()
}
}
}
func (s *AuthInfoStore) GetLoginStats(ctx context.Context) (LoginStats, error) {
var stats LoginStats
outerErr := s.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
rawSQL := `SELECT
(SELECT COUNT(*) FROM (` + s.duplicateUserEntriesSQL(ctx) + `) AS d WHERE (d.dup_login IS NOT NULL OR d.dup_email IS NOT NULL)) as duplicate_user_entries,
(SELECT COUNT(*) FROM (` + s.mixedCasedUsers(ctx) + `) AS mcu) AS mixed_cased_users
`
_, err := dbSession.SQL(rawSQL).Get(&stats)
return err
})
if outerErr != nil {
return stats, outerErr
}
// set prometheus metrics stats
MStatDuplicateUserEntries.Set(float64(stats.DuplicateUserEntries))
if stats.DuplicateUserEntries == 0 {
MStatHasDuplicateEntries.Set(float64(0))
} else {
MStatHasDuplicateEntries.Set(float64(1))
}
MStatMixedCasedUsers.Set(float64(stats.MixedCasedUsers))
return stats, nil
}
func (s *AuthInfoStore) CollectLoginStats(ctx context.Context) (map[string]interface{}, error) {
m := map[string]interface{}{}
loginStats, err := s.GetLoginStats(ctx)
if err != nil {
s.logger.Error("Failed to get login stats", "error", err)
return nil, err
}
m["stats.users.duplicate_user_entries"] = loginStats.DuplicateUserEntries
if loginStats.DuplicateUserEntries > 0 {
m["stats.users.has_duplicate_user_entries"] = 1
} else {
m["stats.users.has_duplicate_user_entries"] = 0
}
m["stats.users.mixed_cased_users"] = loginStats.MixedCasedUsers
return m, nil
}
func (s *AuthInfoStore) duplicateUserEntriesSQL(ctx context.Context) string {
userDialect := s.sqlStore.GetDialect().Quote("user")
// this query counts how many users have the same login or email.
// which might be confusing, but gives a good indication
// we want this query to not require too much cpu
sqlQuery := `SELECT
(SELECT login from ` + userDialect + ` WHERE (LOWER(login) = LOWER(u.login)) AND (login != u.login)) AS dup_login,
(SELECT email from ` + userDialect + ` WHERE (LOWER(email) = LOWER(u.email)) AND (email != u.email)) AS dup_email
FROM ` + userDialect + ` AS u`
return sqlQuery
}
func (s *AuthInfoStore) mixedCasedUsers(ctx context.Context) string {
userDialect := s.sqlStore.GetDialect().Quote("user")
// this query counts how many users have upper case and lower case login or emails.
// why
// users login via IDP or service providers get upper cased domains at times :shrug:
sqlQuery := `SELECT login, email FROM ` + userDialect + ` WHERE (LOWER(login) != login OR lower(email) != email)`
return sqlQuery
}

View File

@@ -0,0 +1,56 @@
package database
import (
"context"
"github.com/grafana/grafana/pkg/services/sqlstore"
)
type loginStats struct {
DuplicateUserEntries int `xorm:"duplicate_user_entries"`
}
func (s *AuthInfoStore) GetLoginStats(ctx context.Context) (loginStats, error) {
var stats loginStats
outerErr := s.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
rawSQL := `SELECT COUNT(*) as duplicate_user_entries FROM (` + s.duplicateUserEntriesSQL(ctx) + `)`
_, err := dbSession.SQL(rawSQL).Get(&stats)
return err
})
if outerErr != nil {
return stats, outerErr
}
return stats, nil
}
func (s *AuthInfoStore) CollectLoginStats(ctx context.Context) (map[string]interface{}, error) {
m := map[string]interface{}{}
loginStats, err := s.GetLoginStats(ctx)
if err != nil {
s.logger.Error("Failed to get login stats", "error", err)
return nil, err
}
m["stats.users.duplicate_user_entries"] = loginStats.DuplicateUserEntries
if loginStats.DuplicateUserEntries > 0 {
m["stats.users.has_duplicate_user_entries"] = 1
} else {
m["stats.users.has_duplicate_user_entries"] = 0
}
return m, nil
}
func (s *AuthInfoStore) duplicateUserEntriesSQL(ctx context.Context) string {
userDialect := s.sqlStore.GetDialect().Quote("user")
// this query counts how many users have the same login or email.
// which might be confusing, but gives a good indication
// we want this query to not require too much cpu
sqlQuery := `SELECT
(SELECT login from ` + userDialect + ` WHERE (LOWER(login) = LOWER(u.login)) AND (login != u.login)) AS dup_login,
(SELECT email from ` + userDialect + ` WHERE (LOWER(email) = LOWER(u.email)) AND (email != u.email)) AS dup_email
FROM ` + userDialect + ` AS u
WHERE (dup_login IS NOT NULL OR dup_email IS NOT NULL)
`
return sqlQuery
}

View File

@@ -195,8 +195,3 @@ func (s *Implementation) SetAuthInfo(ctx context.Context, cmd *models.SetAuthInf
func (s *Implementation) GetExternalUserInfoByLogin(ctx context.Context, query *models.GetExternalUserInfoByLoginQuery) error {
return s.authInfoStore.GetExternalUserInfoByLogin(ctx, query)
}
func (s *Implementation) Run(ctx context.Context) error {
s.logger.Debug("Started AuthInfo Metrics collection service")
return s.authInfoStore.RunMetricsCollection(ctx)
}

View File

@@ -370,24 +370,6 @@ func TestUserAuth(t *testing.T) {
require.Nil(t, user)
})
t.Run("should be able to run query in all dbs", func(t *testing.T) {
// Restore after destructive operation
sqlStore = sqlstore.InitTestDB(t)
for i := 0; i < 5; i++ {
cmd := models.CreateUserCommand{
Email: fmt.Sprint("user", i, "@test.com"),
Name: fmt.Sprint("user", i),
Login: fmt.Sprint("loginuser", i),
OrgId: 1,
}
_, err := sqlStore.CreateUser(context.Background(), cmd)
require.Nil(t, err)
}
_, err := srv.authInfoStore.GetLoginStats(context.Background())
require.Nil(t, err)
})
t.Run("calculate metrics on duplicate userstats", func(t *testing.T) {
// Restore after destructive operation
sqlStore = sqlstore.InitTestDB(t)
@@ -421,14 +403,11 @@ func TestUserAuth(t *testing.T) {
}
_, err = sqlStore.CreateUser(context.Background(), dupUserLogincmd)
require.NoError(t, err)
// require stats to populate
// require metrics and statistics to be 2
m, err := srv.authInfoStore.CollectLoginStats(context.Background())
require.NoError(t, err)
require.Equal(t, 2, m["stats.users.duplicate_user_entries"])
require.Equal(t, 1, m["stats.users.has_duplicate_user_entries"])
require.Equal(t, 1, m["stats.users.mixed_cased_users"])
}
})
})

View File

@@ -4,7 +4,6 @@ import (
"context"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/login/authinfoservice/database"
)
type UserProtectionService interface {
@@ -22,6 +21,4 @@ type Store interface {
GetUserByLogin(ctx context.Context, login string) (*models.User, error)
GetUserByEmail(ctx context.Context, email string) (*models.User, error)
CollectLoginStats(ctx context.Context) (map[string]interface{}, error)
RunMetricsCollection(ctx context.Context) error
GetLoginStats(ctx context.Context) (database.LoginStats, error)
}

View File

@@ -5,7 +5,6 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/channels"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/channels_config"
)
// swagger:route GET /api/v1/provisioning/contact-points provisioning stable RouteGetContactpoints
@@ -119,17 +118,43 @@ func (e *EmbeddedContactPoint) Valid(decryptFunc channels.GetDecryptedValueFn) e
}
func (e *EmbeddedContactPoint) SecretKeys() ([]string, error) {
notifiers := channels_config.GetAvailableNotifiers()
for _, n := range notifiers {
if n.Type == e.Type {
secureFields := []string{}
for _, field := range n.Options {
if field.Secure {
secureFields = append(secureFields, field.PropertyName)
}
}
return secureFields, nil
}
switch e.Type {
case "alertmanager":
return []string{"basicAuthPassword"}, nil
case "dingding":
return []string{}, nil
case "discord":
return []string{}, nil
case "email":
return []string{}, nil
case "googlechat":
return []string{}, nil
case "kafka":
return []string{}, nil
case "line":
return []string{"token"}, nil
case "opsgenie":
return []string{"apiKey"}, nil
case "pagerduty":
return []string{"integrationKey"}, nil
case "pushover":
return []string{"userKey", "apiToken"}, nil
case "sensugo":
return []string{"apiKey"}, nil
case "slack":
return []string{"url", "token"}, nil
case "teams":
return []string{}, nil
case "telegram":
return []string{"bottoken"}, nil
case "threema":
return []string{"api_secret"}, nil
case "victorops":
return []string{}, nil
case "webhook":
return []string{}, nil
case "wecom":
return []string{"url"}, nil
}
return nil, fmt.Errorf("no secrets configured for type '%s'", e.Type)
}

View File

@@ -25,7 +25,7 @@ import (
)
var searchRegex = regexp.MustCompile(`\{(\w+)\}`)
var errInvalidRecipientFormat = errors.New("invalid recipient (datasource) identifier format. Only integer is expected")
var NotImplementedResp = ErrResp(http.StatusNotImplemented, errors.New("endpoint not implemented"), "")
func toMacaronPath(path string) string {
@@ -37,8 +37,7 @@ func toMacaronPath(path string) string {
func backendTypeByUID(ctx *models.ReqContext, cache datasources.CacheService) (apimodels.Backend, error) {
datasourceUID := web.Params(ctx.Req)[":DatasourceUID"]
ds, err := cache.GetDatasourceByUID(ctx.Req.Context(), datasourceUID, ctx.SignedInUser, ctx.SkipCache)
if err == nil {
if ds, err := cache.GetDatasourceByUID(ctx.Req.Context(), datasourceUID, ctx.SignedInUser, ctx.SkipCache); err == nil {
switch ds.Type {
case "loki", "prometheus":
return apimodels.LoTexRulerBackend, nil
@@ -48,7 +47,7 @@ func backendTypeByUID(ctx *models.ReqContext, cache datasources.CacheService) (a
return 0, fmt.Errorf("unexpected backend type (%v)", ds.Type)
}
}
return 0, errors.New("no datasource was found matching the given UID")
return 0, fmt.Errorf("unexpected backend type (%v)", datasourceUID)
}
// macaron unsafely asserts the http.ResponseWriter is an http.CloseNotifier, which will panic.
@@ -101,7 +100,7 @@ func (p *AlertingProxy) withReq(
if datasourceID != "" {
recipient, err := strconv.ParseInt(web.Params(ctx.Req)[":DatasourceID"], 10, 64)
if err != nil {
return ErrResp(http.StatusBadRequest, errInvalidRecipientFormat, "")
return ErrResp(http.StatusBadRequest, err, "DatasourceID is invalid")
}
p.DataProxy.ProxyDatasourceRequestWithID(newCtx, recipient)

View File

@@ -1,4 +1,4 @@
package channels_config
package notifier
import (
"github.com/grafana/grafana/pkg/services/alerting"
@@ -112,7 +112,7 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin {
Heading: "DingDing settings",
Options: []alerting.NotifierOption{
{
Label: "URL",
Label: "Url",
Element: alerting.ElementTypeInput,
InputType: alerting.InputTypeText,
Placeholder: "https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxx",
@@ -276,7 +276,7 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin {
Heading: "VictorOps settings",
Options: []alerting.NotifierOption{
{
Label: "URL",
Label: "Url",
Element: alerting.ElementTypeInput,
InputType: alerting.InputTypeText,
Placeholder: "VictorOps url",
@@ -631,14 +631,14 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin {
Heading: "Webhook settings",
Options: []alerting.NotifierOption{
{
Label: "URL",
Label: "Url",
Element: alerting.ElementTypeInput,
InputType: alerting.InputTypeText,
PropertyName: "url",
Required: true,
},
{
Label: "HTTP Method",
Label: "Http Method",
Element: alerting.ElementTypeSelect,
SelectOptions: []alerting.SelectOption{
{
@@ -653,34 +653,18 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin {
PropertyName: "httpMethod",
},
{
Label: "HTTP Basic Authentication - Username",
Label: "Username",
Element: alerting.ElementTypeInput,
InputType: alerting.InputTypeText,
PropertyName: "username",
},
{
Label: "HTTP Basic Authentication - Password",
Label: "Password",
Element: alerting.ElementTypeInput,
InputType: alerting.InputTypePassword,
PropertyName: "password",
Secure: true,
},
{ // New in 9.1
Label: "Authorization Header - Scheme",
Description: "Optionally provide a scheme for the Authorization Request Header. Default is Bearer.",
Element: alerting.ElementTypeInput,
InputType: alerting.InputTypeText,
PropertyName: "authorization_scheme",
Placeholder: "Bearer",
},
{ // New in 9.1
Label: "Authorization Header - Credentials",
Description: "Credentials for the Authorization Request header. Only one of HTTP Basic Authentication or Authorization Request Header can be set.",
Element: alerting.ElementTypeInput,
InputType: alerting.InputTypeText,
PropertyName: "authorization_credentials",
Secure: true,
},
{ // New in 8.0. TODO: How to enforce only numbers?
Label: "Max Alerts",
Description: "Max alerts to include in a notification. Remaining alerts in the same batch will be ignored above this number. 0 means no limit.",
@@ -697,7 +681,7 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin {
Heading: "WeCom settings",
Options: []alerting.NotifierOption{
{
Label: "URL",
Label: "Url",
Element: alerting.ElementTypeInput,
InputType: alerting.InputTypeText,
Placeholder: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxxxx",
@@ -786,7 +770,7 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin {
Heading: "Google Hangouts Chat settings",
Options: []alerting.NotifierOption{
{
Label: "URL",
Label: "Url",
Element: alerting.ElementTypeInput,
InputType: alerting.InputTypeText,
Placeholder: "Google Hangouts Chat incoming webhook url",
@@ -872,7 +856,7 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin {
Secure: true,
},
{
Label: "Alert API URL",
Label: "Alert API Url",
Element: alerting.ElementTypeInput,
InputType: alerting.InputTypeText,
Placeholder: "https://api.opsgenie.com/v2/alerts",

View File

@@ -4,17 +4,15 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
"github.com/prometheus/common/model"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
"github.com/prometheus/common/model"
)
// WebhookNotifier is responsible for sending
@@ -22,6 +20,8 @@ import (
type WebhookNotifier struct {
*Base
URL string
User string
Password string
HTTPMethod string
MaxAlerts int
log log.Logger
@@ -29,26 +29,15 @@ type WebhookNotifier struct {
images ImageStore
tmpl *template.Template
orgID int64
User string
Password string
AuthorizationScheme string
AuthorizationCredentials string
}
type WebhookConfig struct {
*NotificationChannelConfig
URL string
User string
Password string
HTTPMethod string
MaxAlerts int
// Authorization Header.
AuthorizationScheme string
AuthorizationCredentials string
// HTTP Basic Authentication.
User string
Password string
}
func WebHookFactory(fc FactoryConfig) (NotificationChannel, error) {
@@ -67,23 +56,11 @@ func NewWebHookConfig(config *NotificationChannelConfig, decryptFunc GetDecrypte
if url == "" {
return nil, errors.New("could not find url property in settings")
}
user := config.Settings.Get("username").MustString()
password := decryptFunc(context.Background(), config.SecureSettings, "password", config.Settings.Get("password").MustString())
authorizationScheme := config.Settings.Get("authorization_scheme").MustString("Bearer")
authorizationCredentials := decryptFunc(context.Background(), config.SecureSettings, "authorization_credentials", config.Settings.Get("authorization_credentials").MustString())
if user != "" && password != "" && authorizationScheme != "" && authorizationCredentials != "" {
return nil, errors.New("both HTTP Basic Authentication and Authorization Header are set, only 1 is permitted")
}
return &WebhookConfig{
NotificationChannelConfig: config,
URL: url,
User: user,
Password: password,
AuthorizationScheme: authorizationScheme,
AuthorizationCredentials: authorizationCredentials,
User: config.Settings.Get("username").MustString(),
Password: decryptFunc(context.Background(), config.SecureSettings, "password", config.Settings.Get("password").MustString()),
HTTPMethod: config.Settings.Get("httpMethod").MustString("POST"),
MaxAlerts: config.Settings.Get("maxAlerts").MustInt(0),
}, nil
@@ -100,18 +77,16 @@ func NewWebHookNotifier(config *WebhookConfig, ns notifications.WebhookSender, i
DisableResolveMessage: config.DisableResolveMessage,
Settings: config.Settings,
}),
orgID: config.OrgID,
URL: config.URL,
User: config.User,
Password: config.Password,
AuthorizationScheme: config.AuthorizationScheme,
AuthorizationCredentials: config.AuthorizationCredentials,
HTTPMethod: config.HTTPMethod,
MaxAlerts: config.MaxAlerts,
log: log.New("alerting.notifier.webhook"),
ns: ns,
images: images,
tmpl: t,
orgID: config.OrgID,
URL: config.URL,
User: config.User,
Password: config.Password,
HTTPMethod: config.HTTPMethod,
MaxAlerts: config.MaxAlerts,
log: log.New("alerting.notifier.webhook"),
ns: ns,
images: images,
tmpl: t,
}
}
@@ -177,18 +152,12 @@ func (wn *WebhookNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool
return false, err
}
headers := make(map[string]string)
if wn.AuthorizationScheme != "" && wn.AuthorizationCredentials != "" {
headers["Authorization"] = fmt.Sprintf("%s %s", wn.AuthorizationScheme, wn.AuthorizationCredentials)
}
cmd := &models.SendWebhookSync{
Url: wn.URL,
User: wn.User,
Password: wn.Password,
Body: string(body),
HttpMethod: wn.HTTPMethod,
HttpHeader: headers,
}
if err := wn.ns.SendWebhookSync(ctx, cmd); err != nil {

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