Merge branch 'main' into iwysiu/47597

This commit is contained in:
Isabella Siu
2022-04-26 11:41:43 -04:00
committed by GitHub
201 changed files with 4212 additions and 1701 deletions
+7 -7
View File
@@ -1222,7 +1222,7 @@ depends_on:
- main-build-e2e-publish
- main-integration-tests
kind: pipeline
name: windows-main
name: main-windows
platform:
arch: amd64
os: windows
@@ -1310,9 +1310,9 @@ depends_on:
- main-test
- main-build-e2e-publish
- main-integration-tests
- windows-main
- main-windows
kind: pipeline
name: publish-main
name: main-publish
node:
type: no-parallel
platform:
@@ -1364,10 +1364,10 @@ depends_on:
- main-test
- main-build-e2e-publish
- main-integration-tests
- windows-main
- publish-main
- main-windows
- main-publish
kind: pipeline
name: notify-main
name: main-notify
platform:
arch: amd64
os: linux
@@ -4536,6 +4536,6 @@ kind: secret
name: gcp_upload_artifacts_key
---
kind: signature
hmac: 9380beb722ab1276e9219ae41fa07c84db1e7dc249f69e42386f868c026e1795
hmac: 48f6a3ade75e5993b4a49d8f1a5b7d184a63767a108b16c5f71eca6c986da9c7
...
+1
View File
@@ -58,6 +58,7 @@ go.sum @grafana/backend-platform
/pkg/services/live/ @grafana/grafana-edge-squad
/pkg/services/searchV2/ @grafana/grafana-edge-squad
/pkg/services/store/ @grafana/grafana-edge-squad
/pkg/services/export/ @grafana/grafana-edge-squad
/pkg/infra/filestore/ @grafana/grafana-edge-squad
pkg/tsdb/testdatasource/sims/ @grafana/grafana-edge-squad
+20 -1
View File
@@ -608,7 +608,7 @@ allow_sign_up = true
# LDAP background sync (Enterprise only)
# At 1 am every day
sync_cron = "0 0 1 * * *"
sync_cron = "0 1 * * *"
active_sync_enabled = true
#################################### AWS ###########################
@@ -1176,3 +1176,22 @@ default_baselayer_config =
# Enable or disable loading other base map layers
enable_custom_baselayers = true
#################################### Dashboard previews #####################################
[dashboard_previews.crawler]
# Number of dashboards rendered in parallel. Default is 6.
thread_count =
# Timeout passed down to the Image Renderer plugin. It is used in two separate places within a single rendering request:
# First during the initial navigation to the dashboard and then when waiting for all the panels to load. Default is 20s.
# This setting should be expressed as a duration. Examples: 10s (seconds), 1m (minutes).
rendering_timeout =
# Maximum duration of a single crawl. Default is 1h.
# This setting should be expressed as a duration. Examples: 10s (seconds), 1m (minutes).
max_crawl_duration =
# Minimum interval between two subsequent scheduler runs. Default is 12h.
# This setting should be expressed as a duration. Examples: 10s (seconds), 1m (minutes).
scheduler_interval =
+1 -1
View File
@@ -591,7 +591,7 @@
# LDAP background sync (Enterprise only)
# At 1 am every day
;sync_cron = "0 0 1 * * *"
;sync_cron = "0 1 * * *"
;active_sync_enabled = true
#################################### AWS ###########################
@@ -1371,6 +1371,14 @@ Configures the Profile section.
Enable or disable the Profile section. Default is `enabled`.
## [query_history]
Configures Query history in Explore.
### enabled
Enable or disable the Query history. Default is `disabled`.
## [metrics]
For detailed instructions, refer to [Internal Grafana metrics]({{< relref "view-server/internal-metrics.md" >}}).
@@ -20,17 +20,15 @@ First, you need to set up MySQL or Postgres on another server and configure Graf
You can find the configuration for doing that in the [[database]]({{< relref "../administration/configuration.md#database" >}}) section in the Grafana config.
Grafana will now persist all long term data in the database. How to configure the database for high availability is out of scope for this guide. We recommend finding an expert on the database you're using.
## Alerting
## Alerting high availability
**Grafana 8 alerts**
Grafana alerting provides a new [highly-available model]({{< relref "../alerting/unified-alerting/high-availability/_index.md" >}}). It also preserves the semantics of legacy dashboard alerting by executing all alerts on every server and by sending notifications only once per alert. Load distribution between servers is not supported at this time.
Grafana 8 Alerts provides a new highly-available model under the hood. It preserves the previous semantics by executing all alerts on every server and notifications are sent only once per alert. There is no support for load distribution between servers at this time.
For configuration, [follow the guide]({{< relref "../alerting/unified-alerting/high-availability.md" >}}).
For instructions on setting up alerting high availability, see [enable alerting high availability]({{< relref "../alerting/unified-alerting/high-availability/enable-alerting-ha.md" >}}).
**Legacy dashboard alerts**
Legacy Grafana alerting supports a limited form of high availability. [Alert notifications]({{< relref "../alerting/old-alerting/notifications.md" >}}) are deduplicated when running multiple servers. This means all alerts are executed on every server but alert notifications are only sent once per alert. Grafana does not support load distribution between servers.
Legacy Grafana alerting supports a limited form of high availability. In this model, [alert notifications]({{< relref "../alerting/old-alerting/notifications.md" >}}) are deduplicated when running multiple servers. This means all alerts are executed on every server, but alert notifications are only sent once per alert. Grafana does not support load distribution between servers.
## Grafana Live
@@ -8,7 +8,7 @@ weight = 113
Grafana 8.0 has new and improved alerting that centralizes alerting information in a single, searchable view. It is enabled by default for all new OSS instances, and is an [opt-in]({{< relref "./opt-in.md" >}}) feature for older installations that still use legacy dashboard alerting. We encourage you to create issues in the Grafana GitHub repository for bugs found while testing Grafana alerting. See also, [What's New with Grafana alerting]({{< relref "./difference-old-new.md" >}}).
> Refer to [Fine-grained access control]({{< relref "../enterprise/access-control/_index.md" >}}) in Grafana Enterprise to learn more about controlling access to alerts using fine-grained permissions.
> Refer to [Fine-grained access control]({{< relref "../../enterprise/access-control/_index.md" >}}) in Grafana Enterprise to learn more about controlling access to alerts using fine-grained permissions.
When Grafana alerting is enabled, you can:
@@ -1,44 +0,0 @@
+++
title = " High availability"
description = "High Availability"
keywords = ["grafana", "alerting", "tutorials", "ha", "high availability"]
weight = 450
+++
# High availability
The Grafana alerting system has two main components: a `Scheduler` and an internal `Alertmanager`. The `Scheduler` is responsible for the evaluation of your [alert rules]({{< relref "./fundamentals/evaluate-grafana-alerts.md" >}}) while the internal Alertmanager takes care of the **routing** and **grouping**.
When it comes to running Grafana alerting in high availability the operational mode of the scheduler is unaffected such that all alerts continue be evaluated in each Grafana instance. Rather the operational change happens in the Alertmanager which **deduplicates** alert notifications across Grafana instances.
{{< figure src="/static/img/docs/alerting/unified/high-availability-ua.png" class="docs-image--no-shadow" max-width= "750px" caption="High availability" >}}
The coordination between Grafana instances happens via [a Gossip protocol](https://en.wikipedia.org/wiki/Gossip_protocol). Alerts are not gossiped between instances. It is expected that each scheduler delivers the same alerts to each Alertmanager.
The two types of messages that are gossiped between instances are:
- Notification logs: Who (which instance) notified what (which alert)
- Silences: If an alert should fire or not
These two states are persisted in the database periodically and when Grafana is gracefully shutdown.
## Enable high availability
To enable high availability support you need to add at least 1 Grafana instance to the [`[ha_peer]` configuration option]({{<relref"../../administration/configuration.md#unified_alerting">}}) within the `[unified_alerting]` section:
1. In your custom configuration file ($WORKING_DIR/conf/custom.ini), go to the `[unified_alerting]` section.
2. Set `[ha_peers]` to the number of hosts for each grafana instance in the cluster (using a format of host:port) e.g. `ha_peers=10.0.0.5:9094,10.0.0.6:9094,10.0.0.7:9094`
3. Gossiping of notifications and silences uses both TCP and UDP port 9094. Each Grafana instance will need to be able to accept incoming connections on these ports.
4. Set `[ha_listen_address]` to the instance IP address using a format of host:port (or the [Pod's](https://kubernetes.io/docs/concepts/workloads/pods/) IP in the case of using Kubernetes) by default it is set to listen to all interfaces (`0.0.0.0`).
## Kubernetes
If you are using Kubernetes, you can expose the pod IP [through an environment variable](https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information/) via the container definition such as:
```bash
env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
```
@@ -0,0 +1,25 @@
+++
title = " About alerting high availability"
description = "High availability"
keywords = ["grafana", "alerting", "tutorials", "ha", "high availability"]
weight = 450
+++
# About alerting high availability
The Grafana alerting system has two main components: a `Scheduler` and an internal `Alertmanager`. The `Scheduler` evaluates your [alert rules]({{< relref "../fundamentals/evaluate-grafana-alerts.md" >}}), while the internal Alertmanager manages **routing** and **grouping**.
When running Grafana alerting in high availability, the operational mode of the scheduler remains unaffected, and each Grafana instance evaluates all alerts. The operational change happens in the Alertmanager when it deduplicates alert notifications across Grafana instances.
{{< figure src="/static/img/docs/alerting/unified/high-availability-ua.png" class="docs-image--no-shadow" max-width= "750px" caption="High availability" >}}
The coordination between Grafana instances happens via [a Gossip protocol](https://en.wikipedia.org/wiki/Gossip_protocol). Alerts are not gossiped between instances and each scheduler delivers the same volume of alerts to each Alertmanager.
The two types of messages gossiped between Grafana instances are:
- Notification logs: Who (which instance) notified what (which alert).
- Silences: If an alert should fire or not.
The notification logs and silences are persisted in the database periodically and during a graceful Grafana shut down.
For configuration instructions, refer to [enable alerting high availability]({{< relref "./enable-alerting-ha.md" >}}).
@@ -0,0 +1,36 @@
+++
title = "Enable alerting high availability"
description = "Enable alerting high availability"
keywords = ["grafana", "alerting", "tutorials", "ha", "high availability"]
weight = 450
+++
# Enable alerting high availability
You can enable [alerting high availability]({{< relref "./_index.md" >}}) support by updating the Grafana configuration file. On Kubernetes, you can enable alerting high availability by updating the Kubernetes container definition.
## Update Grafana configuration file
### Before you begin
Since gossiping of notifications and silences uses both TCP and UDP port `9094`, ensure that each Grafana instance is able to accept incoming connections on these ports.
**To enable high availability support:**
1. In your custom configuration file ($WORKING_DIR/conf/custom.ini), go to the `[unified_alerting]` section.
2. Set `[ha_peers]` to the number of hosts for each Grafana instance in the cluster (using a format of host:port), for example, `ha_peers=10.0.0.5:9094,10.0.0.6:9094,10.0.0.7:9094`.
You must have at least one (1) Grafana instance added to the [`[ha_peer]` section.
3. Set `[ha_listen_address]` to the instance IP address using a format of `host:port` (or the [Pod's](https://kubernetes.io/docs/concepts/workloads/pods/) IP in the case of using Kubernetes).
By default, it is set to listen to all interfaces (`0.0.0.0`).
## Update Kubernetes container definition
If you are using Kubernetes, you can expose the pod IP [through an environment variable](https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information/) via the container definition such as:
```bash
env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
```
@@ -46,7 +46,7 @@ How can you tell you are here?
- Compare like to like: split service dashboards when the magnitude differs. Make sure aggregated metrics don't drown out important information.
- Expressive charts with meaningful use of color and normalizing axes where you can.
- Example of meaningful color: Blue means it's good, red means it's bad. [Thresholds]({{< relref "../panels/specify-thresholds/about-thresholds.md" >}}) can help with that.
- Example of meaningful color: Blue means it's good, red means it's bad. [Thresholds]({{< relref "../panels/configure-thresholds" >}}) can help with that.
- Example of normalizing axes: When comparing CPU usage, measure by percentage rather than raw number, because machines can have a different number of cores. Normalizing CPU usage by the number of cores reduces cognitive load because the viewer can trust that at 100% all cores are being used, without having to know the number of CPUs.
- Directed browsing cuts down on "guessing."
- Template variables make it harder to “just browse” randomly or aimlessly.
+3 -3
View File
@@ -48,9 +48,9 @@ Removed users are automatically logged out and their account disabled. These acc
# @weekly | Run once a week, midnight between Sat/Sun | 0 0 0 * * 0
# @daily (or @midnight) | Run once a day, midnight | 0 0 0 * * *
# @hourly | Run once an hour, beginning of hour | 0 0 * * * *
sync_cron = "0 0 1 * * *" # This is default value (At 1 am every day)
# This cron expression format uses 6 space-separated fields (including seconds), for example
# sync_cron = "* */10 * * * *"
sync_cron = "0 1 * * *" # This is default value (At 1 am every day)
# This cron expression format uses 5 space-separated fields, for example
# sync_cron = "*/10 * * * *"
# This will run the LDAP Synchronization every 10th minute, which is also the minimal interval between the Grafana sync times i.e. you cannot set it for every 9th minute
# You can also disable active LDAP synchronization
@@ -13,7 +13,7 @@ Grafana supports user authentication through Okta, which is useful when you want
## Before you begin
- To configure SAML integration with Okta, create integration inside the Okta organization first. [Add integration in Okta](https://help.okta.com/en/prod/Content/Topics/Apps/apps-overview-add-apps.htm)
- Ensure you have permission to administer SAML authentication. For more information about permissions, refer to [About users and permissions]({{< relref "../manage-users-and-permissions/about-users-and-permissions.md#">}}).
- Ensure you have permission to administer SAML authentication. For more information about permissions, refer to [About users and permissions]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions.md#">}}).
**To set up SAML with Okta:**
@@ -0,0 +1,90 @@
---
title: 'Configure visualization thresholds'
menuTitle: 'Configure visualization thresholds'
description: 'This section includes information about using thresholds in your visualizations.'
weight: 300
aliases:
[
docs/grafana/latest/panels/thresholds/,
docs/grafana/latest/panels/,
docs/grafana/latest/panels/specify-thresholds/about-thresholds/,
docs/grafana/latest/panels/specify-thresholds/add-a-threshold/,
docs/grafana/latest/panels/specify-thresholds/add-threshold-to-graph/,
docs/grafana/latest/panels/specify-thresholds/delete-a-threshold/,
]
---
# Configure visualization thresholds
This section includes information about using thresholds in your visualizations. You'll learn about thresholds and their defaults, how to add or delete a threshold, and adding a threshold to a legacy panel.
## About thresholds
A threshold is a value that you specify for a metric that is visually reflected in a dashboard when the threshold value is met or exceeded.
Thresholds provide one method for you to conditionally style and color your visualizations based on query results. You can apply thresholds to most, but not all, visualizations. For more information about visualizations, refer to [Visualization panels]({{< relref "../../visualizations" >}}).
You can use thresholds to:
- Color grid lines or grid ares areas in the [Time-series visualization]({{< relref "../../visualizations/time-series" >}})
- Color lines in the [Time-series visualization]({{< relref "../../visualizations/time-series/graph-color-scheme/#from-thresholds" >}})
- Color the background or value text in the [Stat visualization]({{< relref "../../visualizations/stat-panel" >}})
- Color the gauge and threshold markers in the [Gauge visualization]({{< relref "../../visualizations/gauge-panel" >}})
- Color markers in the [Geomap visualization]({{< relref "../../visualizations/geomap" >}})
- Color cell text or background in the [Table visualization]({{< relref "../../visualizations/table" >}})
- Define regions and region colors in the [State timeline visualization]({{< relref "../../visualizations/state-timeline" >}})
There are two types of thresholds:
- **Absolute** thresholds are defined by a number. For example, 80 on a scale of 1 to 150.
- **Percentage** thresholds are defined relative to minimum or maximum. For example, 80 percent.
### Default thresholds
On visualizations that support it, Grafana sets default threshold values of:
- 80 = red
- Base = green
- Mode = Absolute
The **Base** value represents minus infinity. It is generally the “good” color.
## Add or delete a threshold
You can add as many thresholds to a panel as you want. Grafana automatically sorts thresholds values from highest to lowest.
Delete a threshold when it is no longer relevant to your business operations. When you delete a threshold, the system removes the threshold from all visualizations that include the threshold.
1. To add a threshold:
a. Edit the panel to which you want to add a threshold.
b. In the options side pane, locate the **Thresholds** section and click **+ Add threshold**.
c. Select a threshold color, number, and mode.
Threshold mode applies to all thresholds on this panel.
d. For a time-series panel, select a **Show thresholds** option.
1. To delete a threshold, navigate to the panel that contains the threshold and click the trash icon next to the threshold you want to remove.
## Add a threshold to a legacy graph panel
In the Graph panel visualization, thresholds enable you to add lines or sections to a graph to make it easier to recognize when the graph crosses a threshold.
1. Navigate to the graph panel to which you want to add a threshold.
1. On the **Panel** tab, click **Thresholds**.
1. Click **Add threshold**.
1. Complete the following fields:
- **T1 -** Both values are required to display a threshold.
- **lt** or **gt** - Select **lt** for less than or **gt** for greater than to indicate what the threshold applies to.
- **Value -** Enter a threshold value. Grafana draws a threshold line along the Y-axis at that value.
- **Color -** Choose a condition that corresponds to a color, or define your own color.
- **custom -** You define the fill color and line color.
- **critical -** Fill and line color are red.
- **warning -** Fill and line color are yellow.
- **ok -** Fill and line color are green.
- **Fill -** Controls whether the threshold fill is displayed.
- **Line -** Controls whether the threshold line is displayed.
- **Y-Axis -** Choose **left** or **right**.
1. Click **Save** to save the changes in the dashboard.
@@ -1,11 +0,0 @@
+++
title = "Modify visualization text and background colors"
aliases = ["/docs/grafana/next/panels/thresholds/", "/docs/sources/panels/specify-thresholds/"]
weight = 300
+++
# Modify visualization text and background colors
Use thresholds to set the color of a visualization text and background.
{{< section >}}
@@ -1,26 +0,0 @@
+++
title = "About thresholds"
weight = 10
aliases = ["/docs/sources/panels/specify-thresholds/about-thresholds/"]
+++
# About thresholds
Thresholds set the color of either the value text or the background based on conditions that you define.
There are two types of thresholds:
- **Absolute** thresholds are defined based on a number. For example, 80 on a scale of 1 to 150.
- **Percentage** thresholds are defined relative to minimum or maximum. For example, 80 percent.
You can apply thresholds to most, but not all, visualizations.
## Default thresholds
On visualizations that support it, Grafana sets default threshold values of:
- 80 = red
- Base = green
- Mode = Absolute
The **Base** value represents minus infinity. It is generally the “good” color.
@@ -1,29 +0,0 @@
+++
title = "Add a threshold"
weight = 20
aliases = ["/docs/sources/panels/specify-thresholds/add-a-threshold/"]
+++
# Add a threshold
You can add as many thresholds to a panel as you want. Grafana automatically sorts thresholds from highest value to lowest.
## Before you begin
- [Add a panel to a dashboard]({{< relref "../working-with-panels/add-panel.md" >}}).
**To add a threshold**:
1. Edit the panel to which you want to add a threshold.
1. On the panel display options, locate the **Thresholds** section.
1. Click **+ Add threshold**.
Grafana adds a threshold value and color.
1. Accept the recommendations or edit the threshold.
- **Edit color:** To select a color, click the color dot.
- **Edit number:** To change the threshold value, click in the field and enter a number.
1. Select a **Threshold mode**.
Threshold mode applies to all thresholds on this panel.
1. In the **Show thresholds** drop-down, select a threshold display option.
1. Click **Save**.
@@ -1,32 +0,0 @@
+++
title = "Add a threshold to a legacy graph panel"
weight = 40
aliases = ["/docs/sources/panels/specify-thresholds/add-threshold-to-graph/"]
+++
# Add a threshold to a legacy graph panel
In the Graph panel visualization, thresholds enable you to add lines or sections to a graph to make it easier to recognize when the graph crosses a threshold.
## Before you begin
- [Add a panel to a dashboard]({{< relref "../working-with-panels/add-panel.md" >}}).
**To add a threshold to a graph panel**:
1. Navigate to the graph panel to which you want to add a threshold.
1. On the **Panel** tab, click **Thresholds**.
1. Click **Add threshold**.
1. Complete the following fields:
- **T1 -** Both values are required to display a threshold.
- **lt** or **gt** - Select **lt** for less than or **gt** for greater than to indicate what the threshold applies to.
- **Value -** Enter a threshold value. Grafana draws a threshold line along the Y-axis at that value.
- **Color -** Choose a condition that corresponds to a color, or define your own color.
- **custom -** You define the fill color and line color.
- **critical -** Fill and line color are red.
- **warning -** Fill and line color are yellow.
- **ok -** Fill and line color are green.
- **Fill -** Controls whether the threshold fill is displayed.
- **Line -** Controls whether the threshold line is displayed.
- **Y-Axis -** Choose **left** or **right**.
1. Click **Save** to save the changes in the dashboard.
@@ -1,20 +0,0 @@
+++
title = "Delete a threshold"
weight = 30
aliases = ["/docs/sources/panels/specify-thresholds/delete-a-threshold/"]
+++
# Delete a threshold
Delete a threshold when it is no longer relevant to your business operations. When you delete a threshold, the system removes the threshold from all visualizations that include the threshold.
## Before you begin
- [Add a threshold]({{< relref "./add-a-threshold.md" >}}).
**To delete a threshold**:
1. Navigate to the panel to which you want to add a threshold.
1. Click the **Field** tab. (Or **Panel** tab for a graph panel.)
1. Click the trash can icon next to the threshold you want to remove.
1. Click **Save** to save the changes in the dashboard.
@@ -47,7 +47,7 @@ Panels allow you to show your data in visual form. Each panel needs at least one
- [Format data using value mapping]({{< relref "../format-data/about-value-mapping.md" >}})
- [Visualization-specific options]({{< relref "../../visualizations/_index.md" >}})
- [Override field values]({{< relref "../override-field-values/about-field-overrides.md" >}})
- [Specify thresholds to set the color of visualization text and background]({{< relref "../specify-thresholds/about-thresholds.md" >}})
- [Configure thresholds]({{< relref "../configure-thresholds/" >}})
- [Apply color to series and fields]({{< relref "./apply-color-to-series.md" >}})
1. Add a note to describe the visualization (or describe your changes) and then click **Save** in the upper-right corner of the page.
+1 -1
View File
@@ -18,7 +18,7 @@ Graph visualizations allow you to apply:
- [Alerts]({{< relref "../alerting/_index.md" >}}) - This is the only type of visualization that allows you to set alerts.
- [Transform data]({{< relref "../panels/transform-data/add-transformation-to-data.md" >}})
- [Add a field override]({{< relref "../panels/override-field-values/add-a-field-override.md" >}})
- [Add a threshold]({{< relref "../panels/specify-thresholds/add-a-threshold.md" >}})
- [Configure thresholds]({{< relref "../panels/configure-thresholds/" >}})
## Display options
+1 -1
View File
@@ -214,7 +214,7 @@ This release includes a series of features that build on our new usage analytics
### SAML Role and Team Sync
SAML support in Grafana Enterprise is improved by adding Role and Team Sync. Read more about how to use these features in the [SAML team sync documentation]({{< relref "../enterprise/saml.md#configure-team-sync" >}}).
SAML support in Grafana Enterprise is improved by adding Role and Team Sync. Read more about how to use these features in the [SAML team sync documentation]({{< relref "../enterprise/saml/configure-saml.md#configure-team-sync" >}}).
### Okta OAuth Team Sync
+1 -1
View File
@@ -202,7 +202,7 @@ For more information, refer to [Export logs of usage insights]({{< relref "../en
### New audit log events
New log out events are logged based on when a token expires or is revoked, as well as [SAML Single Logout]({{< relref "../enterprise/saml.md#single-logout" >}}). A `tokenId` field was added to all audit logs to help understand which session was logged out of.
New log out events are logged based on when a token expires or is revoked, as well as [SAML Single Logout]({{< relref "../enterprise/saml/configure-saml.md#single-logout" >}}). A `tokenId` field was added to all audit logs to help understand which session was logged out of.
Also, a counter for audit log writing actions with status (success / failure) and logger (loki / file / console) labels was added.
-1
View File
@@ -83,7 +83,6 @@ require (
github.com/prometheus/client_model v0.2.0
github.com/prometheus/common v0.32.1
github.com/prometheus/prometheus v1.8.2-0.20211011171444-354d8d2ecfac
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967
github.com/robfig/cron/v3 v3.0.1
github.com/russellhaering/goxmldsig v1.1.1
github.com/stretchr/testify v1.7.1
+1 -1
View File
@@ -250,7 +250,7 @@
"@grafana/aws-sdk": "0.0.35",
"@grafana/data": "workspace:*",
"@grafana/e2e-selectors": "workspace:*",
"@grafana/experimental": "^0.0.2-canary.25",
"@grafana/experimental": "^0.0.2-canary.30",
"@grafana/google-sdk": "0.0.3",
"@grafana/lezer-logql": "^0.0.11",
"@grafana/runtime": "workspace:*",
@@ -39,6 +39,12 @@ export interface ThemeComponents {
sidemenu: {
width: number;
};
menuTabs: {
height: number;
};
horizontalDrawer: {
defaultHeight: number;
};
}
export function createComponents(colors: ThemeColors, shadows: ThemeShadows): ThemeComponents {
@@ -82,5 +88,11 @@ export function createComponents(colors: ThemeColors, shadows: ThemeShadows): Th
sidemenu: {
width: 48,
},
menuTabs: {
height: 41,
},
horizontalDrawer: {
defaultHeight: 400,
},
};
}
@@ -80,6 +80,59 @@ describe('Rename By Regex Transformer', () => {
});
});
it('should be able to replace globally', async () => {
const cfg: DataTransformerConfig<RenameByRegexTransformerOptions> = {
id: DataTransformerID.renameByRegex,
options: {
regex: '/e/g',
renamePattern: 'E',
},
};
await expect(transformDataFrame([cfg], [data])).toEmitValuesWith((received) => {
const data = received[0];
const frame = data[0];
expect(frame.fields).toMatchInlineSnapshot(`
Array [
Object {
"config": Object {
"displayName": "TimE",
"name": "Time",
},
"name": "Time",
"state": Object {
"displayName": "TimE",
"multipleFrames": false,
},
"type": "time",
"values": Array [
3000,
4000,
5000,
6000,
],
},
Object {
"config": Object {
"displayName": "wEb-01.ExamplE.com",
},
"name": "Value",
"state": Object {
"displayName": "wEb-01.ExamplE.com",
"multipleFrames": false,
},
"type": "number",
"values": Array [
10000.3,
10000.4,
10000.5,
10000.6,
],
},
]
`);
});
});
it('should not rename misses', async () => {
const cfg: DataTransformerConfig<RenameByRegexTransformerOptions> = {
id: DataTransformerID.renameByRegex,
@@ -147,7 +200,6 @@ describe('Rename By Regex Transformer', () => {
Array [
Object {
"config": Object {
"displayName": "Time",
"name": "Time",
},
"name": "Time",
@@ -1,6 +1,7 @@
import { map } from 'rxjs/operators';
import { getFieldDisplayName } from '../../field/fieldState';
import { stringToJsRegex } from '../../text/string';
import { DataFrame } from '../../types/dataFrame';
import { DataTransformerInfo } from '../../types/transformations';
@@ -47,7 +48,7 @@ export const renameByRegexTransformer: DataTransformerInfo<RenameByRegexTransfor
};
const renameFieldsByRegex = (options: RenameByRegexTransformerOptions) => (frame: DataFrame) => {
const regex = new RegExp(options.regex);
const regex = stringToJsRegex(options.regex);
const fields = frame.fields.map((field) => {
const displayName = getFieldDisplayName(field, frame);
if (!regex.test(displayName)) {
@@ -153,6 +153,7 @@ export interface GrafanaConfig {
alertingMinInterval: number;
authProxyEnabled: boolean;
exploreEnabled: boolean;
queryHistoryEnabled: boolean;
helpEnabled: boolean;
profileEnabled: boolean;
ldapEnabled: boolean;
@@ -416,6 +416,7 @@ export type ExploreQueryFieldProps<
export interface QueryEditorHelpProps<TQuery extends DataQuery = DataQuery> {
datasource: DataSourceApi<TQuery>;
query: TQuery;
onClickExample: (query: TQuery) => void;
exploreId?: any;
}
@@ -22,7 +22,6 @@ export interface FeatureToggles {
serviceAccounts?: boolean;
database_metrics?: boolean;
dashboardPreviews?: boolean;
dashboardPreviewsScheduler?: boolean;
dashboardPreviewsAdmin?: boolean;
['live-config']?: boolean;
['live-pipeline']?: boolean;
@@ -51,9 +50,12 @@ export interface FeatureToggles {
saveDashboardDrawer?: boolean;
storage?: boolean;
alertProvisioning?: boolean;
export?: boolean;
storageLocalUpload?: boolean;
azureMonitorResourcePickerForMetrics?: boolean;
explore2Dashboard?: boolean;
tracing?: boolean;
persistNotifications?: boolean;
commandPalette?: boolean;
savedItems?: boolean;
}
@@ -1,4 +1,4 @@
import { e2e } from '../index';
import { e2e } from '..';
import { getScenarioContext } from '../support/scenarioContext';
import { selectOption } from './selectOption';
@@ -148,7 +148,7 @@ export const configurePanel = (config: PartialAddPanelConfig | PartialEditPanelC
//e2e().wait('@chartData');
// Avoid annotations flakiness
e2e.components.RefreshPicker.runButton().should('be.visible').click();
e2e.components.RefreshPicker.runButtonV2().first().should('be.visible').click({ force: true });
e2e().wait('@chartData');
+1
View File
@@ -47,6 +47,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
angularSupportEnabled = false;
authProxyEnabled = false;
exploreEnabled = false;
queryHistoryEnabled = false;
helpEnabled = false;
profileEnabled = false;
ldapEnabled = false;
@@ -97,11 +97,12 @@ export const Field: React.FC<FieldProps> = ({
label
);
const childProps = deleteUndefinedProps({ invalid, disabled, loading });
return (
<div className={cx(styles.field, horizontal && styles.fieldHorizontal, className)} {...otherProps}>
{labelElement}
<div>
{React.cloneElement(children, { invalid, disabled, loading })}
{React.cloneElement(children, childProps)}
{invalid && error && !horizontal && (
<div
className={cx(styles.fieldValidationWrapper, {
@@ -125,3 +126,13 @@ export const Field: React.FC<FieldProps> = ({
</div>
);
};
function deleteUndefinedProps<T extends Object>(obj: T): Partial<T> {
for (const key in obj) {
if (obj[key] === undefined) {
delete obj[key];
}
}
return obj;
}
@@ -1,6 +1,8 @@
import {
ArrayVector,
createTheme,
DashboardCursorSync,
DataFrame,
DefaultTimeZone,
EventBusSrv,
FieldConfig,
@@ -204,4 +206,301 @@ describe('GraphNG utils', () => {
}).getConfig();
expect(result).toMatchSnapshot();
});
test('preparePlotFrame appends min bar spaced nulls when > 1 bar series', () => {
const df1: DataFrame = {
name: 'A',
length: 5,
fields: [
{
name: 'time',
type: FieldType.time,
config: {},
values: new ArrayVector([1, 2, 4, 6, 100]), // should find smallest delta === 1 from here
},
{
name: 'value',
type: FieldType.number,
config: {
custom: {
drawStyle: GraphDrawStyle.Bars,
},
},
values: new ArrayVector([1, 1, 1, 1, 1]),
},
],
};
const df2: DataFrame = {
name: 'B',
length: 5,
fields: [
{
name: 'time',
type: FieldType.time,
config: {},
values: new ArrayVector([30, 40, 50, 90, 100]), // should be appended with two smallest-delta increments
},
{
name: 'value',
type: FieldType.number,
config: {
custom: {
drawStyle: GraphDrawStyle.Bars,
},
},
values: new ArrayVector([2, 2, 2, 2, 2]), // bar series should be appended with nulls
},
{
name: 'value',
type: FieldType.number,
config: {
custom: {
drawStyle: GraphDrawStyle.Line,
},
},
values: new ArrayVector([3, 3, 3, 3, 3]), // line series should be appended with undefineds
},
],
};
const df3: DataFrame = {
name: 'C',
length: 2,
fields: [
{
name: 'time',
type: FieldType.time,
config: {},
values: new ArrayVector([1, 1.1]), // should not trip up on smaller deltas of non-bars
},
{
name: 'value',
type: FieldType.number,
config: {
custom: {
drawStyle: GraphDrawStyle.Line,
},
},
values: new ArrayVector([4, 4]),
},
{
name: 'value',
type: FieldType.number,
config: {
custom: {
drawStyle: GraphDrawStyle.Bars,
hideFrom: {
viz: true, // should ignore hidden bar series
},
},
},
values: new ArrayVector([4, 4]),
},
],
};
let aligndFrame = preparePlotFrame([df1, df2, df3], {
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
y: fieldMatchers.get(FieldMatcherID.numeric).get({}),
});
expect(aligndFrame).toMatchInlineSnapshot(`
Object {
"fields": Array [
Object {
"config": Object {},
"name": "time",
"state": Object {
"origin": Object {
"fieldIndex": 0,
"frameIndex": 0,
},
},
"type": "time",
"values": Array [
1,
1.1,
2,
4,
6,
30,
40,
50,
90,
100,
101,
102,
],
},
Object {
"config": Object {
"custom": Object {
"drawStyle": "bars",
"spanNulls": -1,
},
},
"labels": Object {
"name": "A",
},
"name": "value",
"state": Object {
"origin": Object {
"fieldIndex": 1,
"frameIndex": 0,
},
},
"type": "number",
"values": Array [
1,
undefined,
1,
1,
1,
undefined,
undefined,
undefined,
undefined,
1,
null,
null,
],
},
Object {
"config": Object {
"custom": Object {
"drawStyle": "bars",
"spanNulls": -1,
},
},
"labels": Object {
"name": "B",
},
"name": "value",
"state": Object {
"origin": Object {
"fieldIndex": 1,
"frameIndex": 1,
},
},
"type": "number",
"values": Array [
undefined,
undefined,
undefined,
undefined,
undefined,
2,
2,
2,
2,
2,
null,
null,
],
},
Object {
"config": Object {
"custom": Object {
"drawStyle": "line",
},
},
"labels": Object {
"name": "B",
},
"name": "value",
"state": Object {
"origin": Object {
"fieldIndex": 2,
"frameIndex": 1,
},
},
"type": "number",
"values": Array [
undefined,
undefined,
undefined,
undefined,
undefined,
3,
3,
3,
3,
3,
undefined,
undefined,
],
},
Object {
"config": Object {
"custom": Object {
"drawStyle": "line",
},
},
"labels": Object {
"name": "C",
},
"name": "value",
"state": Object {
"origin": Object {
"fieldIndex": 1,
"frameIndex": 2,
},
},
"type": "number",
"values": Array [
4,
4,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
],
},
Object {
"config": Object {
"custom": Object {
"drawStyle": "bars",
"hideFrom": Object {
"viz": true,
},
},
},
"labels": Object {
"name": "C",
},
"name": "value",
"state": Object {
"origin": Object {
"fieldIndex": 2,
"frameIndex": 2,
},
},
"type": "number",
"values": Array [
4,
4,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
],
},
],
"length": 12,
}
`);
});
});
@@ -1,4 +1,4 @@
import { ArrayVector, DataFrame, FieldConfig, FieldType, outerJoinDataFrames, TimeRange } from '@grafana/data';
import { ArrayVector, DataFrame, Field, FieldConfig, FieldType, outerJoinDataFrames, TimeRange } from '@grafana/data';
import {
AxisPlacement,
GraphDrawStyle,
@@ -12,6 +12,12 @@ import { applyNullInsertThreshold } from './nullInsertThreshold';
import { nullToUndefThreshold } from './nullToUndefThreshold';
import { XYFieldMatchers } from './types';
function isVisibleBarField(f: Field) {
return (
f.type === FieldType.number && f.config.custom?.drawStyle === GraphDrawStyle.Bars && !f.config.custom?.hideFrom?.viz
);
}
// will mutate the DataFrame's fields' values
function applySpanNullsThresholds(frame: DataFrame) {
let refField = frame.fields.find((field) => field.type === FieldType.time); // this doesnt need to be time, just any numeric/asc join field
@@ -20,7 +26,7 @@ function applySpanNullsThresholds(frame: DataFrame) {
for (let i = 0; i < frame.fields.length; i++) {
let field = frame.fields[i];
if (field === refField) {
if (field === refField || isVisibleBarField(field)) {
continue;
}
@@ -37,30 +43,78 @@ function applySpanNullsThresholds(frame: DataFrame) {
}
export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers, timeRange?: TimeRange | null) {
let alignedFrame = outerJoinDataFrames({
frames: frames.map((frame) => {
let fr = applyNullInsertThreshold(frame, null, timeRange?.to.valueOf());
// apply null insertions at interval
frames = frames.map((frame) => applyNullInsertThreshold(frame, null, timeRange?.to.valueOf()));
// prevent minesweeper-expansion of nulls (gaps) when joining bars
// since bar width is determined from the minimum distance between non-undefined values
// (this strategy will still retain any original pre-join nulls, though)
fr.fields.forEach((f) => {
if (f.type === FieldType.number && f.config.custom?.drawStyle === GraphDrawStyle.Bars) {
f.config.custom = {
...f.config.custom,
spanNulls: -1,
};
let numBarSeries = 0;
frames.forEach((frame) => {
frame.fields.forEach((f) => {
if (isVisibleBarField(f)) {
// prevent minesweeper-expansion of nulls (gaps) when joining bars
// since bar width is determined from the minimum distance between non-undefined values
// (this strategy will still retain any original pre-join nulls, though)
f.config.custom = {
...f.config.custom,
spanNulls: -1,
};
numBarSeries++;
}
});
});
// to make bar widths of all series uniform (equal to narrowest bar series), find smallest distance between x points
let minXDelta = Infinity;
if (numBarSeries > 1) {
frames.forEach((frame) => {
if (!frame.fields.some(isVisibleBarField)) {
return;
}
const xVals = frame.fields[0].values.toArray();
for (let i = 0; i < xVals.length; i++) {
if (i > 0) {
minXDelta = Math.min(minXDelta, xVals[i] - xVals[i - 1]);
}
});
}
});
}
return fr;
}),
let alignedFrame = outerJoinDataFrames({
frames,
joinBy: dimFields.x,
keep: dimFields.y,
keepOriginIndices: true,
});
return alignedFrame && applySpanNullsThresholds(alignedFrame);
if (alignedFrame) {
alignedFrame = applySpanNullsThresholds(alignedFrame);
// append 2 null vals at minXDelta to bar series
if (minXDelta !== Infinity) {
alignedFrame.fields.forEach((f, fi) => {
let vals = f.values.toArray();
if (fi === 0) {
let lastVal = vals[vals.length - 1];
vals.push(lastVal + minXDelta, lastVal + 2 * minXDelta);
} else if (isVisibleBarField(f)) {
vals.push(null, null);
} else {
vals.push(undefined, undefined);
}
});
alignedFrame.length += 2;
}
return alignedFrame;
}
return null;
}
export function buildScaleKey(config: FieldConfig<GraphFieldConfig>) {
@@ -28,7 +28,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
tabContent: css`
padding: ${theme.spacing(2)};
background-color: ${theme.colors.background.primary};
height: 100%;
height: calc(100% - ${theme.components.menuTabs.height}px);
`,
close: css`
position: absolute;
@@ -32,6 +32,7 @@ export const Filter: FC<Props> = ({ column, field, tableStyles }) => {
<span
className={cx(tableStyles.headerFilter, filterEnabled ? styles.filterIconEnabled : styles.filterIconDisabled)}
ref={ref}
role="filterIcon"
onClick={onShowPopover}
>
<Icon name="filter" />
@@ -177,4 +177,35 @@ describe('Table', () => {
]);
});
});
describe('on filtering', () => {
it('the rows should be filtered', async () => {
getTestContext({
data: toDataFrame({
name: 'A',
fields: [
{
name: 'number',
type: FieldType.number,
values: [1, 1, 1, 2, 2, 3, 4, 5],
config: {
custom: {
filterable: true,
},
},
},
],
}),
});
expect(within(getTable()).getAllByRole('row')).toHaveLength(9);
await userEvent.click(within(getColumnHeader(/number/)).getByRole('filterIcon'));
await userEvent.click(screen.getByLabelText('1'));
await userEvent.click(screen.getByText('Ok'));
// 3 + header row
expect(within(getTable()).getAllByRole('row')).toHaveLength(4);
});
});
});
@@ -251,7 +251,7 @@ export const Table: FC<Props> = memo((props: Props) => {
[gotoPage]
);
const itemCount = enablePagination ? page.length : data.length;
const itemCount = enablePagination ? page.length : rows.length;
let paginationEl = null;
if (enablePagination) {
const itemsRangeStart = state.pageIndex * state.pageSize + 1;
@@ -23,7 +23,7 @@ const getTabsBarStyles = stylesFactory((theme: GrafanaTheme2, hideBorder = false
tabs: css`
position: relative;
display: flex;
height: 41px;
height: ${theme.components.menuTabs.height}px;
`,
};
});
@@ -230,29 +230,29 @@ describe('preparePlotData2', () => {
],
});
expect(preparePlotData2(df, getStackingGroups(df))).toMatchInlineSnapshot(`
Array [
Array [
9997,
9998,
9999,
],
Array [
-10,
20,
10,
],
Array [
10,
10,
10,
],
Array [
20,
20,
20,
],
]
`);
Array [
Array [
9997,
9998,
9999,
],
Array [
-10,
20,
10,
],
Array [
10,
10,
10,
],
Array [
20,
20,
20,
],
]
`);
});
it('standard', () => {
@@ -289,14 +289,14 @@ describe('preparePlotData2', () => {
10,
],
Array [
0,
30,
20,
10,
10,
10,
],
Array [
20,
50,
40,
30,
30,
30,
],
]
`);
@@ -345,19 +345,19 @@ describe('preparePlotData2', () => {
10,
],
Array [
10,
10,
10,
],
Array [
-30,
0,
30,
20,
-10,
],
Array [
-40,
-10,
-20,
-20,
-20,
],
Array [
-30,
-30,
-30,
],
]
`);
@@ -413,14 +413,14 @@ describe('preparePlotData2', () => {
10,
],
Array [
0,
30,
20,
10,
10,
10,
],
Array [
20,
50,
40,
30,
30,
30,
],
Array [
1,
@@ -580,13 +580,13 @@ describe('auto stacking groups', () => {
"dir": -1,
"series": Array [
1,
3,
],
},
Object {
"dir": 1,
"series": Array [
2,
3,
],
},
]
@@ -622,11 +622,6 @@ describe('auto stacking groups', () => {
"series": Array [
1,
2,
],
},
Object {
"dir": -1,
"series": Array [
3,
],
},
@@ -115,16 +115,17 @@ export function getStackingGroups(frame: DataFrame) {
// will this be stacked up or down after any transforms applied
let vals = values.toArray();
let transform = custom.transform;
let firstValue = vals.find((v) => v != null);
let stackDir =
transform === GraphTransform.Constant
? vals[0] > 0
? firstValue >= 0
? StackDirection.Pos
: StackDirection.Neg
: transform === GraphTransform.NegativeY
? vals.some((v) => v > 0)
? firstValue >= 0
? StackDirection.Neg
: StackDirection.Pos
: vals.some((v) => v > 0)
: firstValue >= 0
? StackDirection.Pos
: StackDirection.Neg;
+4 -5
View File
@@ -12,7 +12,6 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/annotations"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
@@ -275,7 +274,7 @@ func (hs *HTTPServer) MassDeleteAnnotations(c *models.ReqContext) response.Respo
// validations only for RBAC. A user can mass delete all annotations in a (dashboard + panel) or a specific annotation
// if has access to that dashboard.
if hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
if !hs.AccessControl.IsDisabled() {
var dashboardId int64
if cmd.AnnotationId != 0 {
@@ -351,7 +350,7 @@ func (hs *HTTPServer) canSaveAnnotation(c *models.ReqContext, annotation *annota
if annotation.GetType() == annotations.Dashboard {
return canEditDashboard(c, annotation.DashboardId)
} else {
if !hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
if hs.AccessControl.IsDisabled() {
return c.SignedInUser.HasRole(models.ROLE_EDITOR), nil
}
return true, nil
@@ -446,7 +445,7 @@ func AnnotationTypeScopeResolver() (string, accesscontrol.AttributeScopeResolveF
func (hs *HTTPServer) canCreateAnnotation(c *models.ReqContext, dashboardId int64) (bool, error) {
if dashboardId != 0 {
if hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
if !hs.AccessControl.IsDisabled() {
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, accesscontrol.ScopeAnnotationsTypeDashboard)
if canSave, err := hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator); err != nil || !canSave {
return canSave, err
@@ -454,7 +453,7 @@ func (hs *HTTPServer) canCreateAnnotation(c *models.ReqContext, dashboardId int6
}
return canEditDashboard(c, dashboardId)
} else { // organization annotations
if hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
if !hs.AccessControl.IsDisabled() {
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, accesscontrol.ScopeAnnotationsTypeOrganization)
return hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator)
} else {
+5
View File
@@ -523,6 +523,11 @@ func (hs *HTTPServer) registerRoutes() {
adminRoute.Get("/crawler/status", reqGrafanaAdmin, routing.Wrap(hs.ThumbService.CrawlerStatus))
}
if hs.Features.IsEnabled(featuremgmt.FlagExport) {
adminRoute.Get("/export", reqGrafanaAdmin, routing.Wrap(hs.ExportService.HandleGetStatus))
adminRoute.Post("/export", reqGrafanaAdmin, routing.Wrap(hs.ExportService.HandleRequestExport))
}
adminRoute.Post("/provisioning/dashboards/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersDashboards)), routing.Wrap(hs.AdminProvisioningReloadDashboards))
adminRoute.Post("/provisioning/plugins/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersPlugins)), routing.Wrap(hs.AdminProvisioningReloadPlugins))
adminRoute.Post("/provisioning/datasources/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersDatasources)), routing.Wrap(hs.AdminProvisioningReloadDatasources))
+1 -1
View File
@@ -409,7 +409,7 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa
// GetHomeDashboard returns the home dashboard.
func (hs *HTTPServer) GetHomeDashboard(c *models.ReqContext) response.Response {
prefsQuery := pref.GetPreferenceWithDefaultsQuery{UserID: c.SignedInUser.UserId}
prefsQuery := pref.GetPreferenceWithDefaultsQuery{OrgID: c.OrgId, UserID: c.SignedInUser.UserId}
homePage := hs.Cfg.HomePage
preference, err := hs.preferenceService.GetWithDefaults(c.Req.Context(), &prefsQuery)
+1 -2
View File
@@ -11,7 +11,6 @@ import (
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/web"
)
@@ -133,7 +132,7 @@ func (hs *HTTPServer) UpdateDashboardPermissions(c *models.ReqContext) response.
return response.Error(403, "Cannot remove own admin permission for a folder", nil)
}
if hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
if !hs.AccessControl.IsDisabled() {
old, err := g.GetAcl()
if err != nil {
return response.Error(500, "Error while checking dashboard permissions", err)
+1
View File
@@ -38,6 +38,7 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) {
dashboardService: dashboardservice.ProvideDashboardService(
settings, dashboardStore, nil, features, accesscontrolmock.NewPermissionsServicesMock(),
),
AccessControl: accesscontrolmock.New().WithDisabled(),
}
t.Run("Given user has no admin permissions", func(t *testing.T) {
+20 -15
View File
@@ -97,7 +97,7 @@ func (hs *HTTPServer) GetDataSourceById(c *models.ReqContext) response.Response
return response.Error(404, "Data source not found", err)
}
dto := convertModelToDtos(filtered[0])
dto := hs.convertModelToDtos(c.Req.Context(), filtered[0])
// Add accesscontrol metadata
dto.AccessControl = hs.getAccessControlMetadata(c, c.OrgId, datasources.ScopePrefix, dto.UID)
@@ -128,7 +128,7 @@ func (hs *HTTPServer) DeleteDataSourceById(c *models.ReqContext) response.Respon
return response.Error(403, "Cannot delete read-only data source", nil)
}
cmd := &models.DeleteDataSourceCommand{ID: id, OrgID: c.OrgId}
cmd := &models.DeleteDataSourceCommand{ID: id, OrgID: c.OrgId, Name: ds.Name}
err = hs.DataSourcesService.DeleteDataSource(c.Req.Context(), cmd)
if err != nil {
@@ -156,7 +156,7 @@ func (hs *HTTPServer) GetDataSourceByUID(c *models.ReqContext) response.Response
return response.Error(404, "Data source not found", err)
}
dto := convertModelToDtos(filtered[0])
dto := hs.convertModelToDtos(c.Req.Context(), filtered[0])
// Add accesscontrol metadata
dto.AccessControl = hs.getAccessControlMetadata(c, c.OrgId, datasources.ScopePrefix, dto.UID)
@@ -184,7 +184,7 @@ func (hs *HTTPServer) DeleteDataSourceByUID(c *models.ReqContext) response.Respo
return response.Error(403, "Cannot delete read-only data source", nil)
}
cmd := &models.DeleteDataSourceCommand{UID: uid, OrgID: c.OrgId}
cmd := &models.DeleteDataSourceCommand{UID: uid, OrgID: c.OrgId, Name: ds.Name}
err = hs.DataSourcesService.DeleteDataSource(c.Req.Context(), cmd)
if err != nil {
@@ -265,7 +265,7 @@ func (hs *HTTPServer) AddDataSource(c *models.ReqContext) response.Response {
return response.Error(500, "Failed to add datasource", err)
}
ds := convertModelToDtos(cmd.Result)
ds := hs.convertModelToDtos(c.Req.Context(), cmd.Result)
return response.JSON(http.StatusOK, util.DynMap{
"message": "Datasource added",
"id": cmd.Result.Id,
@@ -327,7 +327,7 @@ func (hs *HTTPServer) UpdateDataSource(c *models.ReqContext) response.Response {
return response.Error(500, "Failed to query datasource", err)
}
datasourceDTO := convertModelToDtos(query.Result)
datasourceDTO := hs.convertModelToDtos(c.Req.Context(), query.Result)
hs.Live.HandleDatasourceUpdate(c.OrgId, datasourceDTO.UID)
@@ -408,7 +408,7 @@ func (hs *HTTPServer) GetDataSourceByName(c *models.ReqContext) response.Respons
return response.Error(404, "Data source not found", err)
}
dto := convertModelToDtos(filtered[0])
dto := hs.convertModelToDtos(c.Req.Context(), filtered[0])
return response.JSON(http.StatusOK, &dto)
}
@@ -457,7 +457,7 @@ func (hs *HTTPServer) CallDatasourceResource(c *models.ReqContext) {
hs.callPluginResource(c, plugin.ID, ds.Uid)
}
func convertModelToDtos(ds *models.DataSource) dtos.DataSource {
func (hs *HTTPServer) convertModelToDtos(ctx context.Context, ds *models.DataSource) dtos.DataSource {
dto := dtos.DataSource{
Id: ds.Id,
UID: ds.Uid,
@@ -480,10 +480,15 @@ func convertModelToDtos(ds *models.DataSource) dtos.DataSource {
ReadOnly: ds.ReadOnly,
}
for k, v := range ds.SecureJsonData {
if len(v) > 0 {
dto.SecureJsonFields[k] = true
secrets, err := hs.DataSourcesService.DecryptedValues(ctx, ds)
if err == nil {
for k, v := range secrets {
if len(v) > 0 {
dto.SecureJsonFields[k] = true
}
}
} else {
datasourcesLogger.Debug("Failed to retrieve datasource secrets to parse secure json fields", "error", err)
}
return dto
@@ -510,7 +515,7 @@ func (hs *HTTPServer) CheckDatasourceHealth(c *models.ReqContext) response.Respo
return response.Error(http.StatusInternalServerError, "Unable to find datasource plugin", err)
}
dsInstanceSettings, err := adapters.ModelToInstanceSettings(ds, hs.decryptSecureJsonDataFn())
dsInstanceSettings, err := adapters.ModelToInstanceSettings(ds, hs.decryptSecureJsonDataFn(c.Req.Context()))
if err != nil {
return response.Error(http.StatusInternalServerError, "Unable to get datasource model", err)
}
@@ -561,9 +566,9 @@ func (hs *HTTPServer) CheckDatasourceHealth(c *models.ReqContext) response.Respo
return response.JSON(http.StatusOK, payload)
}
func (hs *HTTPServer) decryptSecureJsonDataFn() func(map[string][]byte) map[string]string {
return func(m map[string][]byte) map[string]string {
decryptedJsonData, err := hs.SecretsService.DecryptJsonData(context.Background(), m)
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)
}
+5
View File
@@ -583,3 +583,8 @@ func (m *dataSourcesServiceMock) UpdateDataSource(ctx context.Context, cmd *mode
cmd.Result = m.expectedDatasource
return m.expectedError
}
func (m *dataSourcesServiceMock) DecryptedValues(ctx context.Context, ds *models.DataSource) (map[string]string, error) {
decryptedValues := make(map[string]string)
return decryptedValues, m.expectedError
}
+1 -1
View File
@@ -52,7 +52,7 @@ func (hs *HTTPServer) GetFolderByID(c *models.ReqContext) response.Response {
if err != nil {
return response.Error(http.StatusBadRequest, "id is invalid", err)
}
folder, err := hs.folderService.GetFolderByID(c.Req.Context(), c.SignedInUser, c.OrgId, id)
folder, err := hs.folderService.GetFolderByID(c.Req.Context(), c.SignedInUser, id, c.OrgId)
if err != nil {
return apierrors.ToFolderErrorResponse(err)
}
+1 -2
View File
@@ -9,7 +9,6 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
@@ -115,7 +114,7 @@ func (hs *HTTPServer) UpdateFolderPermissions(c *models.ReqContext) response.Res
return response.Error(403, "Cannot remove own admin permission for a folder", nil)
}
if hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
if !hs.AccessControl.IsDisabled() {
old, err := g.GetAcl()
if err != nil {
return response.Error(500, "Error while checking dashboard permissions", err)
+1
View File
@@ -44,6 +44,7 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
dashboardService: service.ProvideDashboardService(
settings, dashboardStore, nil, features, permissionsServices,
),
AccessControl: accesscontrolmock.New().WithDisabled(),
}
t.Run("Given folder not exists", func(t *testing.T) {
+18 -3
View File
@@ -248,9 +248,14 @@ func (hs *HTTPServer) getFSDataSources(c *models.ReqContext, enabledPlugins Enab
if ds.Access == models.DS_ACCESS_DIRECT {
if ds.BasicAuth {
password, err := hs.DataSourcesService.DecryptedBasicAuthPassword(c.Req.Context(), ds)
if err != nil {
return nil, err
}
dsDTO.BasicAuth = util.GetBasicAuthHeader(
ds.BasicAuthUser,
hs.DataSourcesService.DecryptedBasicAuthPassword(ds),
password,
)
}
if ds.WithCredentials {
@@ -258,14 +263,24 @@ func (hs *HTTPServer) getFSDataSources(c *models.ReqContext, enabledPlugins Enab
}
if ds.Type == models.DS_INFLUXDB_08 {
password, err := hs.DataSourcesService.DecryptedPassword(c.Req.Context(), ds)
if err != nil {
return nil, err
}
dsDTO.Username = ds.User
dsDTO.Password = hs.DataSourcesService.DecryptedPassword(ds)
dsDTO.Password = password
dsDTO.URL = url + "/db/" + ds.Database
}
if ds.Type == models.DS_INFLUXDB {
password, err := hs.DataSourcesService.DecryptedPassword(c.Req.Context(), ds)
if err != nil {
return nil, err
}
dsDTO.Username = ds.User
dsDTO.Password = hs.DataSourcesService.DecryptedPassword(ds)
dsDTO.Password = password
dsDTO.URL = url
}
}
+4 -1
View File
@@ -39,6 +39,7 @@ import (
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/datasources/permissions"
"github.com/grafana/grafana/pkg/services/encryption"
"github.com/grafana/grafana/pkg/services/export"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/hooks"
"github.com/grafana/grafana/pkg/services/ldap"
@@ -112,6 +113,7 @@ type HTTPServer struct {
Live *live.GrafanaLive
LivePushGateway *pushhttp.Gateway
ThumbService thumbs.Service
ExportService export.ExportService
StorageService store.HTTPStorageService
ContextHandler *contexthandler.ContextHandler
SQLStore sqlstore.Store
@@ -170,7 +172,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
contextHandler *contexthandler.ContextHandler, features *featuremgmt.FeatureManager,
schemaService *schemaloader.SchemaLoaderService, alertNG *ngalert.AlertNG,
libraryPanelService librarypanels.Service, libraryElementService libraryelements.Service,
quotaService *quota.QuotaService, socialService social.Service, tracer tracing.Tracer,
quotaService *quota.QuotaService, socialService social.Service, tracer tracing.Tracer, exportService export.ExportService,
encryptionService encryption.Internal, grafanaUpdateChecker *updatechecker.GrafanaService,
pluginsUpdateChecker *updatechecker.PluginsService, searchUsersService searchusers.Service,
dataSourcesService datasources.DataSourceService, secretsService secrets.Service, queryDataService *query.Service,
@@ -217,6 +219,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
AccessControl: accessControl,
DataProxy: dataSourceProxy,
SearchService: searchService,
ExportService: exportService,
Live: live,
LivePushGateway: livePushGateway,
PluginContextProvider: plugCtxProvider,
+8 -13
View File
@@ -168,12 +168,12 @@ func (hs *HTTPServer) ReqCanAdminTeams(c *models.ReqContext) bool {
return c.OrgRole == models.ROLE_ADMIN || (hs.Cfg.EditorsCanAdmin && c.OrgRole == models.ROLE_EDITOR)
}
func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dtos.NavLink, error) {
func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool, prefs *pref.Preference) ([]*dtos.NavLink, error) {
hasAccess := ac.HasAccess(hs.AccessControl, c)
navTree := []*dtos.NavLink{}
if hs.Features.IsEnabled(featuremgmt.FlagNewNavigation) {
savedItemsLinks, err := hs.buildSavedItemsNavLinks(c)
if hs.Features.IsEnabled(featuremgmt.FlagSavedItems) {
savedItemsLinks, err := hs.buildSavedItemsNavLinks(c, prefs)
if err != nil {
return nil, err
}
@@ -411,16 +411,11 @@ func (hs *HTTPServer) addHelpLinks(navTree []*dtos.NavLink, c *models.ReqContext
return navTree
}
func (hs *HTTPServer) buildSavedItemsNavLinks(c *models.ReqContext) ([]*dtos.NavLink, error) {
func (hs *HTTPServer) buildSavedItemsNavLinks(c *models.ReqContext, prefs *pref.Preference) ([]*dtos.NavLink, error) {
savedItemsChildNavs := []*dtos.NavLink{}
// query preferences table for any saved items
prefsQuery := pref.GetPreferenceWithDefaultsQuery{UserID: c.SignedInUser.UserId}
preference, err := hs.preferenceService.GetWithDefaults(c.Req.Context(), &prefsQuery)
if err != nil {
return nil, err
}
savedItems := preference.JSONData.Navbar.SavedItems
savedItems := prefs.JSONData.Navbar.SavedItems
if len(savedItems) > 0 {
for _, savedItem := range savedItems {
@@ -665,7 +660,7 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
settings["dateFormats"] = hs.Cfg.DateFormats
prefsQuery := pref.GetPreferenceWithDefaultsQuery{UserID: c.SignedInUser.UserId}
prefsQuery := pref.GetPreferenceWithDefaultsQuery{UserID: c.UserId, OrgID: c.OrgId, Teams: c.Teams}
prefs, err := hs.preferenceService.GetWithDefaults(c.Req.Context(), &prefsQuery)
if err != nil {
return nil, err
@@ -690,7 +685,7 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
settings["appSubUrl"] = ""
}
navTree, err := hs.getNavTree(c, hasEditPerm)
navTree, err := hs.getNavTree(c, hasEditPerm, prefs)
if err != nil {
return nil, err
}
@@ -738,7 +733,7 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
LoadingLogo: "public/img/grafana_icon.svg",
}
if hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
if !hs.AccessControl.IsDisabled() {
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser, ac.Options{ReloadCache: false})
if err != nil {
return nil, err
+9 -1
View File
@@ -9,6 +9,7 @@ import (
"strings"
"testing"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
@@ -18,8 +19,11 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
datasources "github.com/grafana/grafana/pkg/services/datasources/service"
"github.com/grafana/grafana/pkg/services/query"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
"github.com/grafana/grafana/pkg/services/secrets/kvstore"
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
"github.com/stretchr/testify/assert"
)
@@ -193,13 +197,17 @@ type dashboardFakePluginClient struct {
func TestAPIEndpoint_Metrics_QueryMetricsFromDashboard(t *testing.T) {
sc := setupHTTPServerWithMockDb(t, false, false)
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
ds := datasources.ProvideService(nil, secretsService, secretsStore, nil, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
setInitCtxSignedInViewer(sc.initCtx)
sc.hs.queryDataService = query.ProvideService(
nil,
nil,
nil,
&fakePluginRequestValidator{},
fakes.NewFakeSecretsService(),
ds,
&dashboardFakePluginClient{},
&fakeOAuthTokenService{},
)
+2 -4
View File
@@ -10,8 +10,6 @@ import (
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
@@ -90,13 +88,13 @@ func (hs *HTTPServer) CreateOrg(c *models.ReqContext) response.Response {
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
acEnabled := hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol)
acEnabled := !hs.AccessControl.IsDisabled()
if !acEnabled && !(setting.AllowUserOrgCreate || c.IsGrafanaAdmin) {
return response.Error(403, "Access denied", nil)
}
cmd.UserId = c.UserId
if err := sqlstore.CreateOrg(c.Req.Context(), &cmd); err != nil {
if err := hs.SQLStore.CreateOrg(c.Req.Context(), &cmd); err != nil {
if errors.Is(err, models.ErrOrgNameTaken) {
return response.Error(409, "Organization name taken", err)
}
+30 -17
View File
@@ -19,7 +19,6 @@ import (
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/oauthtoken"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/util/proxyutil"
@@ -43,7 +42,6 @@ type DataSourceProxy struct {
oAuthTokenService oauthtoken.OAuthTokenService
dataSourcesService datasources.DataSourceService
tracer tracing.Tracer
secretsService secrets.Service
}
type httpClient interface {
@@ -54,7 +52,7 @@ type httpClient interface {
func NewDataSourceProxy(ds *models.DataSource, pluginRoutes []*plugins.Route, ctx *models.ReqContext,
proxyPath string, cfg *setting.Cfg, clientProvider httpclient.Provider,
oAuthTokenService oauthtoken.OAuthTokenService, dsService datasources.DataSourceService,
tracer tracing.Tracer, secretsService secrets.Service) (*DataSourceProxy, error) {
tracer tracing.Tracer) (*DataSourceProxy, error) {
targetURL, err := datasource.ValidateURL(ds.Type, ds.Url)
if err != nil {
return nil, err
@@ -71,7 +69,6 @@ func NewDataSourceProxy(ds *models.DataSource, pluginRoutes []*plugins.Route, ct
oAuthTokenService: oAuthTokenService,
dataSourcesService: dsService,
tracer: tracer,
secretsService: secretsService,
}, nil
}
@@ -97,7 +94,7 @@ func (proxy *DataSourceProxy) HandleRequest() {
"referer", proxy.ctx.Req.Referer(),
)
transport, err := proxy.dataSourcesService.GetHTTPTransport(proxy.ds, proxy.clientProvider)
transport, err := proxy.dataSourcesService.GetHTTPTransport(proxy.ctx.Req.Context(), proxy.ds, proxy.clientProvider)
if err != nil {
proxy.ctx.JsonApiErr(400, "Unable to load TLS certificate", err)
return
@@ -169,17 +166,28 @@ func (proxy *DataSourceProxy) director(req *http.Request) {
switch proxy.ds.Type {
case models.DS_INFLUXDB_08:
password, err := proxy.dataSourcesService.DecryptedPassword(req.Context(), proxy.ds)
if err != nil {
logger.Error("Error interpolating proxy url", "error", err)
return
}
req.URL.RawPath = util.JoinURLFragments(proxy.targetUrl.Path, "db/"+proxy.ds.Database+"/"+proxy.proxyPath)
reqQueryVals.Add("u", proxy.ds.User)
reqQueryVals.Add("p", proxy.dataSourcesService.DecryptedPassword(proxy.ds))
reqQueryVals.Add("p", password)
req.URL.RawQuery = reqQueryVals.Encode()
case models.DS_INFLUXDB:
password, err := proxy.dataSourcesService.DecryptedPassword(req.Context(), proxy.ds)
if err != nil {
logger.Error("Error interpolating proxy url", "error", err)
return
}
req.URL.RawPath = util.JoinURLFragments(proxy.targetUrl.Path, proxy.proxyPath)
req.URL.RawQuery = reqQueryVals.Encode()
if !proxy.ds.BasicAuth {
req.Header.Set(
"Authorization",
util.GetBasicAuthHeader(proxy.ds.User, proxy.dataSourcesService.DecryptedPassword(proxy.ds)),
util.GetBasicAuthHeader(proxy.ds.User, password),
)
}
default:
@@ -195,8 +203,13 @@ func (proxy *DataSourceProxy) director(req *http.Request) {
req.URL.Path = unescapedPath
if proxy.ds.BasicAuth {
password, err := proxy.dataSourcesService.DecryptedBasicAuthPassword(req.Context(), proxy.ds)
if err != nil {
logger.Error("Error interpolating proxy url", "error", err)
return
}
req.Header.Set("Authorization", util.GetBasicAuthHeader(proxy.ds.BasicAuthUser,
proxy.dataSourcesService.DecryptedBasicAuthPassword(proxy.ds)))
password))
}
dsAuth := req.Header.Get("X-DS-Authorization")
@@ -226,23 +239,23 @@ func (proxy *DataSourceProxy) director(req *http.Request) {
}
}
secureJsonData, err := proxy.secretsService.DecryptJsonData(req.Context(), proxy.ds.SecureJsonData)
if err != nil {
logger.Error("Error interpolating proxy url", "error", err)
return
}
if proxy.matchedRoute != nil {
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, DSInfo{
decryptedValues, err := proxy.dataSourcesService.DecryptedValues(req.Context(), proxy.ds)
if err != nil {
logger.Error("Error interpolating proxy url", "error", err)
return
}
ApplyRoute(req.Context(), req, proxy.proxyPath, proxy.matchedRoute, DSInfo{
ID: proxy.ds.Id,
Updated: proxy.ds.Updated,
JSONData: jsonData,
DecryptedSecureJSONData: secureJsonData,
DecryptedSecureJSONData: decryptedValues,
}, proxy.cfg)
}
if proxy.oAuthTokenService.IsOAuthPassThruEnabled(proxy.ds) {
if token := proxy.oAuthTokenService.GetCurrentOAuthToken(proxy.ctx.Req.Context(), proxy.ctx.SignedInUser); token != nil {
if token := proxy.oAuthTokenService.GetCurrentOAuthToken(req.Context(), proxy.ctx.SignedInUser); token != nil {
req.Header.Set("Authorization", fmt.Sprintf("%s %s", token.Type(), token.AccessToken))
idToken, ok := token.Extra("id_token").(string)
+105 -79
View File
@@ -3,6 +3,7 @@ package pluginproxy
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
@@ -24,6 +25,7 @@ import (
"github.com/grafana/grafana/pkg/services/oauthtoken"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
"github.com/grafana/grafana/pkg/services/secrets/kvstore"
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
@@ -90,6 +92,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
})
setting.SecretKey = "password" //nolint:goconst
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
key, err := secretsService.Encrypt(context.Background(), []byte("123"), secrets.WithoutScope())
require.NoError(t, err)
@@ -128,9 +131,9 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("When matching route path", func(t *testing.T) {
ctx, req := setUp()
dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/v4/some/method", cfg, httpClientProvider,
&oauthtoken.Service{}, dsService, tracer, secretsService)
&oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
proxy.matchedRoute = routes[0]
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, cfg)
@@ -141,8 +144,8 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("When matching route path and has dynamic url", func(t *testing.T) {
ctx, req := setUp()
dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/common/some/method", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/common/some/method", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
proxy.matchedRoute = routes[3]
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, cfg)
@@ -153,8 +156,8 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("When matching route path with no url", func(t *testing.T) {
ctx, req := setUp()
dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
proxy.matchedRoute = routes[4]
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, cfg)
@@ -164,8 +167,8 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("When matching route path and has dynamic body", func(t *testing.T) {
ctx, req := setUp()
dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/body", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/body", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
proxy.matchedRoute = routes[5]
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, cfg)
@@ -178,8 +181,8 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("Validating request", func(t *testing.T) {
t.Run("plugin route with valid role", func(t *testing.T) {
ctx, _ := setUp()
dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/v4/some/method", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/v4/some/method", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
err = proxy.validateRequest()
require.NoError(t, err)
@@ -187,8 +190,8 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("plugin route with admin role and user is editor", func(t *testing.T) {
ctx, _ := setUp()
dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/admin", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/admin", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
err = proxy.validateRequest()
require.Error(t, err)
@@ -197,8 +200,8 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("plugin route with admin role and user is admin", func(t *testing.T) {
ctx, _ := setUp()
ctx.SignedInUser.OrgRole = models.ROLE_ADMIN
dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/admin", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/admin", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
err = proxy.validateRequest()
require.NoError(t, err)
@@ -242,6 +245,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
})
setting.SecretKey = "password"
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
key, err := secretsService.Encrypt(context.Background(), []byte("123"), secrets.WithoutScope())
require.NoError(t, err)
@@ -286,8 +290,8 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
},
}
dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken1", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken1", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[0], dsInfo, cfg)
@@ -302,8 +306,8 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
req, err := http.NewRequest("GET", "http://localhost/asd", nil)
require.NoError(t, err)
client = newFakeHTTPClient(t, json2)
dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken2", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken2", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[1], dsInfo, cfg)
@@ -319,8 +323,8 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
require.NoError(t, err)
client = newFakeHTTPClient(t, []byte{})
dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken1", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken1", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[0], dsInfo, cfg)
@@ -340,9 +344,10 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
ds := &models.DataSource{Url: "htttp://graphite:8080", Type: models.DS_GRAPHITE}
ctx := &models.ReqContext{}
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{BuildVersion: "5.3.0"}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{BuildVersion: "5.3.0"}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
require.NoError(t, err)
@@ -366,9 +371,10 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
ctx := &models.ReqContext{}
var routes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
@@ -390,9 +396,10 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
ctx := &models.ReqContext{}
var routes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
requestURL, err := url.Parse("http://grafana.com/sub")
@@ -418,9 +425,10 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
ctx := &models.ReqContext{}
var pluginRoutes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, pluginRoutes, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, pluginRoutes, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
requestURL, err := url.Parse("http://grafana.com/sub")
@@ -441,9 +449,10 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
}
ctx := &models.ReqContext{}
var routes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/to/folder/", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/to/folder/", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
req.Header.Set("Origin", "grafana.com")
@@ -490,9 +499,10 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
}
var routes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/to/folder/", &setting.Cfg{}, httpClientProvider, &mockAuthToken, dsService, tracer, secretsService)
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/to/folder/", &setting.Cfg{}, httpClientProvider, &mockAuthToken, dsService, tracer)
require.NoError(t, err)
req, err = http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
require.NoError(t, err)
@@ -543,24 +553,25 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
})
t.Run("When proxying data source proxy should handle authentication", func(t *testing.T) {
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
tests := []*testCase{
createAuthTest(t, secretsService, models.DS_INFLUXDB_08, "http://localhost:9090", authTypePassword, authCheckQuery, false),
createAuthTest(t, secretsService, models.DS_INFLUXDB_08, "http://localhost:9090", authTypePassword, authCheckQuery, true),
createAuthTest(t, secretsService, models.DS_INFLUXDB, "http://localhost:9090", authTypePassword, authCheckHeader, true),
createAuthTest(t, secretsService, models.DS_INFLUXDB, "http://localhost:9090", authTypePassword, authCheckHeader, false),
createAuthTest(t, secretsService, models.DS_INFLUXDB, "http://localhost:9090", authTypeBasic, authCheckHeader, true),
createAuthTest(t, secretsService, models.DS_INFLUXDB, "http://localhost:9090", authTypeBasic, authCheckHeader, false),
createAuthTest(t, secretsStore, models.DS_INFLUXDB_08, "http://localhost:9090", authTypePassword, authCheckQuery, false),
createAuthTest(t, secretsStore, models.DS_INFLUXDB_08, "http://localhost:9090", authTypePassword, authCheckQuery, true),
createAuthTest(t, secretsStore, models.DS_INFLUXDB, "http://localhost:9090", authTypePassword, authCheckHeader, true),
createAuthTest(t, secretsStore, models.DS_INFLUXDB, "http://localhost:9090", authTypePassword, authCheckHeader, false),
createAuthTest(t, secretsStore, models.DS_INFLUXDB, "http://localhost:9090", authTypeBasic, authCheckHeader, true),
createAuthTest(t, secretsStore, models.DS_INFLUXDB, "http://localhost:9090", authTypeBasic, authCheckHeader, false),
// These two should be enough for any other datasource at the moment. Proxy has special handling
// only for Influx, others have the same path and only BasicAuth. Non BasicAuth datasources
// do not go through proxy but through TSDB API which is not tested here.
createAuthTest(t, secretsService, models.DS_ES, "http://localhost:9200", authTypeBasic, authCheckHeader, false),
createAuthTest(t, secretsService, models.DS_ES, "http://localhost:9200", authTypeBasic, authCheckHeader, true),
createAuthTest(t, secretsStore, models.DS_ES, "http://localhost:9200", authTypeBasic, authCheckHeader, false),
createAuthTest(t, secretsStore, models.DS_ES, "http://localhost:9200", authTypeBasic, authCheckHeader, true),
}
for _, test := range tests {
runDatasourceAuthTest(t, secretsService, cfg, test)
runDatasourceAuthTest(t, secretsService, secretsStore, cfg, test)
}
})
}
@@ -624,9 +635,10 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
t.Run("When response header Set-Cookie is not set should remove proxied Set-Cookie header", func(t *testing.T) {
ctx, ds := setUp(t)
var routes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
proxy.HandleRequest()
@@ -642,9 +654,10 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
},
})
var routes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
proxy.HandleRequest()
@@ -656,9 +669,10 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
t.Run("When response should set Content-Security-Policy header", func(t *testing.T) {
ctx, ds := setUp(t)
var routes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
proxy.HandleRequest()
@@ -678,9 +692,10 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
},
})
var routes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
proxy.HandleRequest()
@@ -703,9 +718,10 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
ctx.Req = httptest.NewRequest("GET", "/api/datasources/proxy/1/path/%2Ftest%2Ftest%2F?query=%2Ftest%2Ftest%2F", nil)
var routes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/%2Ftest%2Ftest%2F", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/%2Ftest%2Ftest%2F", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
proxy.HandleRequest()
@@ -727,9 +743,10 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
ctx.Req = httptest.NewRequest("GET", "/api/datasources/proxy/1/path/%2Ftest%2Ftest%2F?query=%2Ftest%2Ftest%2F", nil)
var routes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/%2Ftest%2Ftest%2F", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/%2Ftest%2Ftest%2F", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
proxy.HandleRequest()
@@ -752,9 +769,10 @@ func TestNewDataSourceProxy_InvalidURL(t *testing.T) {
tracer, err := tracing.InitializeTracerForTest()
require.NoError(t, err)
var routes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
_, err = NewDataSourceProxy(&ds, routes, &ctx, "api/method", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService)
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
_, err = NewDataSourceProxy(&ds, routes, &ctx, "api/method", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer)
require.Error(t, err)
assert.True(t, strings.HasPrefix(err.Error(), `validation of data source URL "://host/root" failed`))
}
@@ -773,9 +791,10 @@ func TestNewDataSourceProxy_ProtocolLessURL(t *testing.T) {
require.NoError(t, err)
var routes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
_, err = NewDataSourceProxy(&ds, routes, &ctx, "api/method", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService)
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
_, err = NewDataSourceProxy(&ds, routes, &ctx, "api/method", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
}
@@ -816,9 +835,10 @@ func TestNewDataSourceProxy_MSSQL(t *testing.T) {
}
var routes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
p, err := NewDataSourceProxy(&ds, routes, &ctx, "api/method", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService)
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
p, err := NewDataSourceProxy(&ds, routes, &ctx, "api/method", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer)
if tc.err == nil {
require.NoError(t, err)
assert.Equal(t, &url.URL{
@@ -843,9 +863,10 @@ func getDatasourceProxiedRequest(t *testing.T, ctx *models.ReqContext, cfg *sett
require.NoError(t, err)
var routes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService)
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
require.NoError(t, err)
@@ -897,15 +918,15 @@ const (
authCheckHeader = "header"
)
func createAuthTest(t *testing.T, secretsService secrets.Service, dsType string, url string, authType string, authCheck string, useSecureJsonData bool) *testCase {
ctx := context.Background()
func createAuthTest(t *testing.T, secretsStore kvstore.SecretsKVStore, dsType string, url string, authType string, authCheck string, useSecureJsonData bool) *testCase {
// Basic user:password
base64AuthHeader := "Basic dXNlcjpwYXNzd29yZA=="
test := &testCase{
datasource: &models.DataSource{
Id: 1,
OrgId: 1,
Name: fmt.Sprintf("%s,%s,%s,%s,%t", dsType, url, authType, authCheck, useSecureJsonData),
Type: dsType,
JsonData: simplejson.New(),
Url: url,
@@ -917,11 +938,13 @@ func createAuthTest(t *testing.T, secretsService secrets.Service, dsType string,
message = fmt.Sprintf("%v should add username and password", dsType)
test.datasource.User = "user"
if useSecureJsonData {
test.datasource.SecureJsonData, err = secretsService.EncryptJsonData(
ctx,
map[string]string{
"password": "password",
}, secrets.WithoutScope())
secureJsonData, err := json.Marshal(map[string]string{
"password": "password",
})
require.NoError(t, err)
err = secretsStore.Set(context.Background(), test.datasource.OrgId, test.datasource.Name, "datasource", string(secureJsonData))
require.NoError(t, err)
} else {
test.datasource.Password = "password"
}
@@ -930,11 +953,13 @@ func createAuthTest(t *testing.T, secretsService secrets.Service, dsType string,
test.datasource.BasicAuth = true
test.datasource.BasicAuthUser = "user"
if useSecureJsonData {
test.datasource.SecureJsonData, err = secretsService.EncryptJsonData(
ctx,
map[string]string{
"basicAuthPassword": "password",
}, secrets.WithoutScope())
secureJsonData, err := json.Marshal(map[string]string{
"basicAuthPassword": "password",
})
require.NoError(t, err)
err = secretsStore.Set(context.Background(), test.datasource.OrgId, test.datasource.Name, "datasource", string(secureJsonData))
require.NoError(t, err)
} else {
test.datasource.BasicAuthPassword = "password"
}
@@ -962,14 +987,14 @@ func createAuthTest(t *testing.T, secretsService secrets.Service, dsType string,
return test
}
func runDatasourceAuthTest(t *testing.T, secretsService secrets.Service, cfg *setting.Cfg, test *testCase) {
func runDatasourceAuthTest(t *testing.T, secretsService secrets.Service, secretsStore kvstore.SecretsKVStore, cfg *setting.Cfg, test *testCase) {
ctx := &models.ReqContext{}
tracer, err := tracing.InitializeTracerForTest()
require.NoError(t, err)
var routes []*plugins.Route
dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(test.datasource, routes, ctx, "", &setting.Cfg{}, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService)
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(test.datasource, routes, ctx, "", &setting.Cfg{}, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
@@ -1010,9 +1035,10 @@ func Test_PathCheck(t *testing.T) {
return ctx, req
}
ctx, _ := setUp()
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(&models.DataSource{}, routes, ctx, "b", &setting.Cfg{}, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService)
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(&models.DataSource{}, routes, ctx, "b", &setting.Cfg{}, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
require.Nil(t, proxy.validateRequest())
+7 -8
View File
@@ -8,7 +8,6 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
@@ -19,7 +18,7 @@ func (hs *HTTPServer) CreateTeam(c *models.ReqContext) response.Response {
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
accessControlEnabled := hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol)
accessControlEnabled := !hs.AccessControl.IsDisabled()
if !accessControlEnabled && c.OrgRole == models.ROLE_VIEWER {
return response.Error(403, "Not allowed to create team.", nil)
}
@@ -63,7 +62,7 @@ func (hs *HTTPServer) UpdateTeam(c *models.ReqContext) response.Response {
return response.Error(http.StatusBadRequest, "teamId is invalid", err)
}
if !hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
if hs.AccessControl.IsDisabled() {
if err := hs.teamGuardian.CanAdmin(c.Req.Context(), cmd.OrgId, cmd.Id, c.SignedInUser); err != nil {
return response.Error(403, "Not allowed to update team", err)
}
@@ -88,7 +87,7 @@ func (hs *HTTPServer) DeleteTeamByID(c *models.ReqContext) response.Response {
}
user := c.SignedInUser
if !hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
if hs.AccessControl.IsDisabled() {
if err := hs.teamGuardian.CanAdmin(c.Req.Context(), orgId, teamId, user); err != nil {
return response.Error(403, "Not allowed to delete team", err)
}
@@ -116,7 +115,7 @@ func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response {
// Using accesscontrol the filtering is done based on user permissions
userIdFilter := models.FilterIgnoreUser
if !hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
if hs.AccessControl.IsDisabled() {
userIdFilter = userFilter(c)
}
@@ -174,7 +173,7 @@ func (hs *HTTPServer) GetTeamByID(c *models.ReqContext) response.Response {
// Using accesscontrol the filtering has already been performed at middleware layer
userIdFilter := models.FilterIgnoreUser
if !hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
if hs.AccessControl.IsDisabled() {
userIdFilter = userFilter(c)
}
@@ -210,7 +209,7 @@ func (hs *HTTPServer) GetTeamPreferences(c *models.ReqContext) response.Response
orgId := c.OrgId
if !hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
if hs.AccessControl.IsDisabled() {
if err := hs.teamGuardian.CanAdmin(c.Req.Context(), orgId, teamId, c.SignedInUser); err != nil {
return response.Error(403, "Not allowed to view team preferences.", err)
}
@@ -233,7 +232,7 @@ func (hs *HTTPServer) UpdateTeamPreferences(c *models.ReqContext) response.Respo
orgId := c.OrgId
if !hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
if hs.AccessControl.IsDisabled() {
if err := hs.teamGuardian.CanAdmin(c.Req.Context(), orgId, teamId, c.SignedInUser); err != nil {
return response.Error(403, "Not allowed to update team preferences.", err)
}
+4 -5
View File
@@ -11,7 +11,6 @@ import (
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
@@ -27,7 +26,7 @@ func (hs *HTTPServer) GetTeamMembers(c *models.ReqContext) response.Response {
// With accesscontrol the permission check has been done at middleware layer
// and the membership filtering will be done at DB layer based on user permissions
if !hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
if hs.AccessControl.IsDisabled() {
if err := hs.teamGuardian.CanAdmin(c.Req.Context(), query.OrgId, query.TeamId, c.SignedInUser); err != nil {
return response.Error(403, "Not allowed to list team members", err)
}
@@ -70,7 +69,7 @@ func (hs *HTTPServer) AddTeamMember(c *models.ReqContext) response.Response {
return response.Error(http.StatusBadRequest, "teamId is invalid", err)
}
if !hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
if hs.AccessControl.IsDisabled() {
if err := hs.teamGuardian.CanAdmin(c.Req.Context(), cmd.OrgId, cmd.TeamId, c.SignedInUser); err != nil {
return response.Error(403, "Not allowed to add team member", err)
}
@@ -110,7 +109,7 @@ func (hs *HTTPServer) UpdateTeamMember(c *models.ReqContext) response.Response {
}
orgId := c.OrgId
if !hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
if hs.AccessControl.IsDisabled() {
if err := hs.teamGuardian.CanAdmin(c.Req.Context(), orgId, teamId, c.SignedInUser); err != nil {
return response.Error(403, "Not allowed to update team member", err)
}
@@ -153,7 +152,7 @@ func (hs *HTTPServer) RemoveTeamMember(c *models.ReqContext) response.Response {
return response.Error(http.StatusBadRequest, "userId is invalid", err)
}
if !hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
if hs.AccessControl.IsDisabled() {
if err := hs.teamGuardian.CanAdmin(c.Req.Context(), orgId, teamId, c.SignedInUser); err != nil {
return response.Error(403, "Not allowed to remove team member", err)
}
+8 -8
View File
@@ -7,7 +7,7 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/setting"
)
@@ -36,16 +36,16 @@ func IsDataSource(uid string) bool {
// Service is service representation for expression handling.
type Service struct {
cfg *setting.Cfg
dataService backend.QueryDataHandler
secretsService secrets.Service
cfg *setting.Cfg
dataService backend.QueryDataHandler
dataSourceService datasources.DataSourceService
}
func ProvideService(cfg *setting.Cfg, pluginClient plugins.Client, secretsService secrets.Service) *Service {
func ProvideService(cfg *setting.Cfg, pluginClient plugins.Client, dataSourceService datasources.DataSourceService) *Service {
return &Service{
cfg: cfg,
dataService: pluginClient,
secretsService: secretsService,
cfg: cfg,
dataService: pluginClient,
dataSourceService: dataSourceService,
}
}
+4 -7
View File
@@ -11,8 +11,7 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
datasources "github.com/grafana/grafana/pkg/services/datasources/fakes"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require"
)
@@ -28,12 +27,10 @@ func TestService(t *testing.T) {
cfg := setting.NewCfg()
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
s := Service{
cfg: cfg,
dataService: me,
secretsService: secretsService,
cfg: cfg,
dataService: me,
dataSourceService: &datasources.FakeDataSourceService{},
}
queries := []Query{
+3 -3
View File
@@ -126,9 +126,9 @@ func hiddenRefIDs(queries []Query) (map[string]struct{}, error) {
return hidden, nil
}
func (s *Service) decryptSecureJsonDataFn(ctx context.Context) func(map[string][]byte) map[string]string {
return func(m map[string][]byte) map[string]string {
decryptedJsonData, err := s.secretsService.DecryptJsonData(ctx, m)
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)
}
-4
View File
@@ -1,9 +1,6 @@
package extensions
import (
// Upgrade ldapsync from cron to cron.v3 and
// remove the cron (v1) dependency
_ "cloud.google.com/go/kms/apiv1"
_ "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
_ "github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys"
@@ -24,7 +21,6 @@ import (
_ "github.com/linkedin/goavro/v2"
_ "github.com/m3db/prometheus_remote_client_golang/promremote"
_ "github.com/pkg/errors"
_ "github.com/robfig/cron"
_ "github.com/robfig/cron/v3"
_ "github.com/russellhaering/goxmldsig"
_ "github.com/stretchr/testify/require"
@@ -60,7 +60,7 @@ func (s *Service) detectPrometheusVariant(ctx context.Context, ds *models.DataSo
} `json:"data"`
}
c, err := s.datasources.GetHTTPTransport(ds, s.httpClientProvider)
c, err := s.datasources.GetHTTPTransport(ctx, ds, s.httpClientProvider)
if err != nil {
s.log.Error("Failed to get HTTP client for Prometheus data source", "error", err)
return "", err
@@ -395,6 +395,6 @@ func (s mockDatasourceService) GetDataSourcesByType(ctx context.Context, query *
return nil
}
func (s mockDatasourceService) GetHTTPTransport(ds *models.DataSource, provider httpclient.Provider, customMiddlewares ...sdkhttpclient.Middleware) (http.RoundTripper, error) {
func (s mockDatasourceService) GetHTTPTransport(ctx context.Context, ds *models.DataSource, provider httpclient.Provider, customMiddlewares ...sdkhttpclient.Middleware) (http.RoundTripper, error) {
return provider.GetTransport()
}
+2 -2
View File
@@ -9,7 +9,7 @@ import (
)
// ModelToInstanceSettings converts a models.DataSource to a backend.DataSourceInstanceSettings.
func ModelToInstanceSettings(ds *models.DataSource, decryptFn func(map[string][]byte) map[string]string,
func ModelToInstanceSettings(ds *models.DataSource, decryptFn func(ds *models.DataSource) map[string]string,
) (*backend.DataSourceInstanceSettings, error) {
var jsonDataBytes json.RawMessage
if ds.JsonData != nil {
@@ -30,7 +30,7 @@ func ModelToInstanceSettings(ds *models.DataSource, decryptFn func(map[string][]
BasicAuthEnabled: ds.BasicAuth,
BasicAuthUser: ds.BasicAuthUser,
JSONData: jsonDataBytes,
DecryptedSecureJSONData: decryptFn(ds.SecureJsonData),
DecryptedSecureJSONData: decryptFn(ds),
Updated: ds.Updated,
}, nil
}
+7 -8
View File
@@ -15,18 +15,17 @@ import (
"github.com/grafana/grafana/pkg/plugins/adapters"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/pluginsettings"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/util/errutil"
)
func ProvideService(cacheService *localcache.CacheService, pluginStore plugins.Store,
dataSourceCache datasources.CacheService, secretsService secrets.Service,
dataSourceCache datasources.CacheService, dataSourceService datasources.DataSourceService,
pluginSettingsService pluginsettings.Service) *Provider {
return &Provider{
cacheService: cacheService,
pluginStore: pluginStore,
dataSourceCache: dataSourceCache,
secretsService: secretsService,
dataSourceService: dataSourceService,
pluginSettingsService: pluginSettingsService,
logger: log.New("plugincontext"),
}
@@ -36,7 +35,7 @@ type Provider struct {
cacheService *localcache.CacheService
pluginStore plugins.Store
dataSourceCache datasources.CacheService
secretsService secrets.Service
dataSourceService datasources.DataSourceService
pluginSettingsService pluginsettings.Service
logger log.Logger
}
@@ -87,7 +86,7 @@ func (p *Provider) Get(ctx context.Context, pluginID string, datasourceUID strin
if err != nil {
return pc, false, errutil.Wrap("Failed to get datasource", err)
}
datasourceSettings, err := adapters.ModelToInstanceSettings(ds, p.decryptSecureJsonDataFn())
datasourceSettings, err := adapters.ModelToInstanceSettings(ds, p.decryptSecureJsonDataFn(ctx))
if err != nil {
return pc, false, errutil.Wrap("Failed to convert datasource", err)
}
@@ -122,9 +121,9 @@ func (p *Provider) getCachedPluginSettings(ctx context.Context, pluginID string,
return ps, nil
}
func (p *Provider) decryptSecureJsonDataFn() func(map[string][]byte) map[string]string {
return func(m map[string][]byte) map[string]string {
decryptedJsonData, err := p.secretsService.DecryptJsonData(context.Background(), m)
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)
}
+4
View File
@@ -50,6 +50,7 @@ import (
"github.com/grafana/grafana/pkg/services/datasourceproxy"
"github.com/grafana/grafana/pkg/services/datasources"
datasourceservice "github.com/grafana/grafana/pkg/services/datasources/service"
"github.com/grafana/grafana/pkg/services/export"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/hooks"
@@ -79,6 +80,7 @@ import (
"github.com/grafana/grafana/pkg/services/searchV2"
"github.com/grafana/grafana/pkg/services/secrets"
secretsDatabase "github.com/grafana/grafana/pkg/services/secrets/database"
secretsStore "github.com/grafana/grafana/pkg/services/secrets/kvstore"
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
"github.com/grafana/grafana/pkg/services/serviceaccounts"
serviceaccountsmanager "github.com/grafana/grafana/pkg/services/serviceaccounts/manager"
@@ -174,6 +176,7 @@ var wireBasicSet = wire.NewSet(
searchV2.ProvideService,
store.ProvideService,
store.ProvideHTTPService,
export.ProvideService,
live.ProvideService,
pushhttp.ProvideService,
plugincontext.ProvideService,
@@ -239,6 +242,7 @@ var wireBasicSet = wire.NewSet(
wire.Bind(new(alerting.DashAlertExtractor), new(*alerting.DashAlertExtractorService)),
comments.ProvideService,
guardian.ProvideService,
secretsStore.ProvideService,
avatar.ProvideAvatarCacheServer,
authproxy.ProvideAuthProxy,
statscollector.ProvideService,
+2 -1
View File
@@ -93,7 +93,8 @@ func ProvideAlertEngine(renderer rendering.Service, requestValidator models.Plug
// Run starts the alerting service background process.
func (e *AlertEngine) Run(ctx context.Context) error {
e.ticker = NewTicker(clock.New(), 1*time.Second, metrics.NewTickerMetrics(prometheus.DefaultRegisterer))
reg := prometheus.WrapRegistererWithPrefix("legacy_", prometheus.DefaultRegisterer)
e.ticker = NewTicker(clock.New(), 1*time.Second, metrics.NewTickerMetrics(reg))
alertGroup, ctx := errgroup.WithContext(ctx)
alertGroup.Go(func() error { return e.alertingTicker(ctx) })
alertGroup.Go(func() error { return e.runJobDispatcher(ctx) })
+16 -2
View File
@@ -137,7 +137,14 @@ func TestTicker(t *testing.T) {
expectedMetric := fmt.Sprintf(expectedMetricFmt, interval.Seconds(), 0, float64(expectedTick.UnixNano())/1e9)
require.NoError(t, testutil.GatherAndCompare(registry, bytes.NewBufferString(expectedMetric), "grafana_alerting_ticker_last_consumed_tick_timestamp_seconds", "grafana_alerting_ticker_next_tick_timestamp_seconds", "grafana_alerting_ticker_interval_seconds"))
errs := make(map[string]error, 1)
require.Eventuallyf(t, func() bool {
err := testutil.GatherAndCompare(registry, bytes.NewBufferString(expectedMetric), "grafana_alerting_ticker_last_consumed_tick_timestamp_seconds", "grafana_alerting_ticker_next_tick_timestamp_seconds", "grafana_alerting_ticker_interval_seconds")
if err != nil {
errs["error"] = err
}
return err == nil
}, 1*time.Second, 100*time.Millisecond, "failed to wait for metrics to match expected values:\n%v", errs)
clk.Add(interval)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
@@ -147,6 +154,13 @@ func TestTicker(t *testing.T) {
actual := readChanOrFail(t, ctx, ticker.C)
expectedMetric = fmt.Sprintf(expectedMetricFmt, interval.Seconds(), float64(actual.UnixNano())/1e9, float64(expectedTick.Add(interval).UnixNano())/1e9)
require.NoError(t, testutil.GatherAndCompare(registry, bytes.NewBufferString(expectedMetric), "grafana_alerting_ticker_last_consumed_tick_timestamp_seconds", "grafana_alerting_ticker_next_tick_timestamp_seconds", "grafana_alerting_ticker_interval_seconds"))
require.Eventuallyf(t, func() bool {
err := testutil.GatherAndCompare(registry, bytes.NewBufferString(expectedMetric), "grafana_alerting_ticker_last_consumed_tick_timestamp_seconds", "grafana_alerting_ticker_next_tick_timestamp_seconds", "grafana_alerting_ticker_interval_seconds")
if err != nil {
errs["error"] = err
}
return err == nil
}, 1*time.Second, 100*time.Millisecond, "failed to wait for metrics to match expected values:\n%v", errs)
})
}
@@ -105,7 +105,7 @@ func (c *PermissionChecker) CheckWritePermissions(ctx context.Context, orgId int
if !c.features.IsEnabled(featuremgmt.FlagAnnotationComments) {
return false, nil
}
if c.features.IsEnabled(featuremgmt.FlagAccesscontrol) {
if !c.accessControl.IsDisabled() {
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsWrite, accesscontrol.ScopeAnnotationsTypeDashboard)
if canEdit, err := c.accessControl.Evaluate(ctx, signedInUser, evaluator); err != nil || !canEdit {
return canEdit, err
@@ -115,7 +115,7 @@ func (p *DataSourceProxyService) proxyDatasourceRequest(c *models.ReqContext, ds
proxyPath := getProxyPath(c)
proxy, err := pluginproxy.NewDataSourceProxy(ds, plugin.Routes, c, proxyPath, p.Cfg, p.HTTPClientProvider,
p.OAuthTokenService, p.DataSourcesService, p.tracer, p.secretsService)
p.OAuthTokenService, p.DataSourcesService, p.tracer)
if err != nil {
if errors.Is(err, datasource.URLValidationError{}) {
c.JsonApiErr(http.StatusBadRequest, fmt.Sprintf("Invalid data source URL: %q", ds.Url), err)
+5 -5
View File
@@ -33,23 +33,23 @@ type DataSourceService interface {
GetDefaultDataSource(ctx context.Context, query *models.GetDefaultDataSourceQuery) error
// GetHTTPTransport gets a datasource specific HTTP transport.
GetHTTPTransport(ds *models.DataSource, provider httpclient.Provider, customMiddlewares ...sdkhttpclient.Middleware) (http.RoundTripper, error)
GetHTTPTransport(ctx context.Context, ds *models.DataSource, provider httpclient.Provider, customMiddlewares ...sdkhttpclient.Middleware) (http.RoundTripper, error)
// DecryptedValues decrypts the encrypted secureJSONData of the provided datasource and
// returns the decrypted values.
DecryptedValues(ds *models.DataSource) map[string]string
DecryptedValues(ctx context.Context, ds *models.DataSource) (map[string]string, error)
// DecryptedValue decrypts the encrypted datasource secureJSONData identified by key
// and returns the decryped value.
DecryptedValue(ds *models.DataSource, key string) (string, bool)
DecryptedValue(ctx context.Context, ds *models.DataSource, key string) (string, bool, error)
// DecryptedBasicAuthPassword decrypts the encrypted datasource basic authentication
// password and returns the decryped value.
DecryptedBasicAuthPassword(ds *models.DataSource) string
DecryptedBasicAuthPassword(ctx context.Context, ds *models.DataSource) (string, error)
// DecryptedPassword decrypts the encrypted datasource password and returns the
// decryped value.
DecryptedPassword(ds *models.DataSource) string
DecryptedPassword(ctx context.Context, ds *models.DataSource) (string, error)
}
// CacheService interface for retrieving a cached datasource.
@@ -4,13 +4,14 @@ import (
"context"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/datasources"
)
type FakeCacheService struct {
DataSources []*models.DataSource
}
var _ CacheService = &FakeCacheService{}
var _ datasources.CacheService = &FakeCacheService{}
func (c *FakeCacheService) GetDatasource(ctx context.Context, datasourceID int64, user *models.SignedInUser, skipCache bool) (*models.DataSource, error) {
for _, datasource := range c.DataSources {
@@ -0,0 +1,124 @@
package datasources
import (
"context"
"net/http"
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/infra/httpclient"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/datasources"
)
type FakeDataSourceService struct {
lastId int64
DataSources []*models.DataSource
}
var _ datasources.DataSourceService = &FakeDataSourceService{}
func (s *FakeDataSourceService) GetDataSource(ctx context.Context, query *models.GetDataSourceQuery) error {
for _, datasource := range s.DataSources {
idMatch := query.Id != 0 && query.Id == datasource.Id
uidMatch := query.Uid != "" && query.Uid == datasource.Uid
nameMatch := query.Name != "" && query.Name == datasource.Name
if idMatch || nameMatch || uidMatch {
query.Result = datasource
return nil
}
}
return models.ErrDataSourceNotFound
}
func (s *FakeDataSourceService) GetDataSources(ctx context.Context, query *models.GetDataSourcesQuery) error {
for _, datasource := range s.DataSources {
orgMatch := query.OrgId != 0 && query.OrgId == datasource.OrgId
if orgMatch {
query.Result = append(query.Result, datasource)
}
}
return nil
}
func (s *FakeDataSourceService) GetDataSourcesByType(ctx context.Context, query *models.GetDataSourcesByTypeQuery) error {
for _, datasource := range s.DataSources {
typeMatch := query.Type != "" && query.Type == datasource.Type
if typeMatch {
query.Result = append(query.Result, datasource)
}
}
return nil
}
func (s *FakeDataSourceService) AddDataSource(ctx context.Context, cmd *models.AddDataSourceCommand) error {
if s.lastId == 0 {
s.lastId = int64(len(s.DataSources) - 1)
}
cmd.Result = &models.DataSource{
Id: s.lastId + 1,
Name: cmd.Name,
Type: cmd.Type,
Uid: cmd.Uid,
OrgId: cmd.OrgId,
}
s.DataSources = append(s.DataSources, cmd.Result)
return nil
}
func (s *FakeDataSourceService) DeleteDataSource(ctx context.Context, cmd *models.DeleteDataSourceCommand) error {
for i, datasource := range s.DataSources {
idMatch := cmd.ID != 0 && cmd.ID == datasource.Id
uidMatch := cmd.UID != "" && cmd.UID == datasource.Uid
nameMatch := cmd.Name != "" && cmd.Name == datasource.Name
if idMatch || nameMatch || uidMatch {
s.DataSources = append(s.DataSources[:i], s.DataSources[i+1:]...)
return nil
}
}
return models.ErrDataSourceNotFound
}
func (s *FakeDataSourceService) UpdateDataSource(ctx context.Context, cmd *models.UpdateDataSourceCommand) error {
for _, datasource := range s.DataSources {
idMatch := cmd.Id != 0 && cmd.Id == datasource.Id
uidMatch := cmd.Uid != "" && cmd.Uid == datasource.Uid
nameMatch := cmd.Name != "" && cmd.Name == datasource.Name
if idMatch || nameMatch || uidMatch {
if cmd.Name != "" {
datasource.Name = cmd.Name
}
return nil
}
}
return models.ErrDataSourceNotFound
}
func (s *FakeDataSourceService) GetDefaultDataSource(ctx context.Context, query *models.GetDefaultDataSourceQuery) error {
return nil
}
func (s *FakeDataSourceService) GetHTTPTransport(ctx context.Context, ds *models.DataSource, provider httpclient.Provider, customMiddlewares ...sdkhttpclient.Middleware) (http.RoundTripper, error) {
rt, err := provider.GetTransport(sdkhttpclient.Options{})
if err != nil {
return nil, err
}
return rt, nil
}
func (s *FakeDataSourceService) DecryptedValues(ctx context.Context, ds *models.DataSource) (map[string]string, error) {
values := make(map[string]string)
return values, nil
}
func (s *FakeDataSourceService) DecryptedValue(ctx context.Context, ds *models.DataSource, key string) (string, bool, error) {
return "", false, nil
}
func (s *FakeDataSourceService) DecryptedBasicAuthPassword(ctx context.Context, ds *models.DataSource) (string, error) {
return "", nil
}
func (s *FakeDataSourceService) DecryptedPassword(ctx context.Context, ds *models.DataSource) (string, error) {
return "", nil
}
@@ -3,6 +3,7 @@ package service
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"net/url"
@@ -23,19 +24,21 @@ import (
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/secrets/kvstore"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
)
type Service struct {
SQLStore *sqlstore.SQLStore
SecretsStore kvstore.SecretsKVStore
SecretsService secrets.Service
cfg *setting.Cfg
features featuremgmt.FeatureToggles
permissionsService accesscontrol.PermissionsService
ac accesscontrol.AccessControl
ptc proxyTransportCache
dsDecryptionCache secureJSONDecryptionCache
ptc proxyTransportCache
}
type proxyTransportCache struct {
@@ -48,32 +51,21 @@ type cachedRoundTripper struct {
roundTripper http.RoundTripper
}
type secureJSONDecryptionCache struct {
cache map[int64]cachedDecryptedJSON
sync.Mutex
}
type cachedDecryptedJSON struct {
updated time.Time
json map[string]string
}
func ProvideService(
store *sqlstore.SQLStore, secretsService secrets.Service, cfg *setting.Cfg, features featuremgmt.FeatureToggles,
ac accesscontrol.AccessControl, permissionsServices accesscontrol.PermissionsServices,
store *sqlstore.SQLStore, secretsService secrets.Service, secretsStore kvstore.SecretsKVStore, cfg *setting.Cfg,
features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl, permissionsServices accesscontrol.PermissionsServices,
) *Service {
s := &Service{
SQLStore: store,
SecretsStore: secretsStore,
SecretsService: secretsService,
ptc: proxyTransportCache{
cache: make(map[int64]cachedRoundTripper),
},
dsDecryptionCache: secureJSONDecryptionCache{
cache: make(map[int64]cachedDecryptedJSON),
},
cfg: cfg,
features: features,
permissionsService: permissionsServices.GetDataSourceService(),
ac: ac,
}
ac.RegisterAttributeScopeResolver(NewNameScopeResolver(store))
@@ -88,6 +80,8 @@ type DataSourceRetriever interface {
GetDataSource(ctx context.Context, query *models.GetDataSourceQuery) error
}
const secretType = "datasource"
// NewNameScopeResolver provides an AttributeScopeResolver able to
// translate a scope prefixed with "datasources:name:" into an uid based scope.
func NewNameScopeResolver(db DataSourceRetriever) (string, accesscontrol.AttributeScopeResolveFunc) {
@@ -153,16 +147,21 @@ func (s *Service) GetDataSourcesByType(ctx context.Context, query *models.GetDat
func (s *Service) AddDataSource(ctx context.Context, cmd *models.AddDataSourceCommand) error {
var err error
cmd.EncryptedSecureJsonData, err = s.SecretsService.EncryptJsonData(ctx, cmd.SecureJsonData, secrets.WithoutScope())
if err != nil {
return err
}
if err := s.SQLStore.AddDataSource(ctx, cmd); err != nil {
return err
}
if s.features.IsEnabled(featuremgmt.FlagAccesscontrol) {
secret, err := json.Marshal(cmd.SecureJsonData)
if err != nil {
return err
}
err = s.SecretsStore.Set(ctx, cmd.OrgId, cmd.Name, secretType, string(secret))
if err != nil {
return err
}
if !s.ac.IsDisabled() {
// This belongs in Data source permissions, and we probably want
// to do this with a hook in the store and rollback on fail.
// We can't use events, because there's no way to communicate
@@ -184,25 +183,50 @@ func (s *Service) AddDataSource(ctx context.Context, cmd *models.AddDataSourceCo
}
func (s *Service) DeleteDataSource(ctx context.Context, cmd *models.DeleteDataSourceCommand) error {
return s.SQLStore.DeleteDataSource(ctx, cmd)
err := s.SQLStore.DeleteDataSource(ctx, cmd)
if err != nil {
return err
}
return s.SecretsStore.Del(ctx, cmd.OrgID, cmd.Name, secretType)
}
func (s *Service) UpdateDataSource(ctx context.Context, cmd *models.UpdateDataSourceCommand) error {
var err error
cmd.EncryptedSecureJsonData, err = s.SecretsService.EncryptJsonData(ctx, cmd.SecureJsonData, secrets.WithoutScope())
secret, err := json.Marshal(cmd.SecureJsonData)
if err != nil {
return err
}
return s.SQLStore.UpdateDataSource(ctx, cmd)
query := &models.GetDataSourceQuery{
Id: cmd.Id,
OrgId: cmd.OrgId,
}
err = s.SQLStore.GetDataSource(ctx, query)
if err != nil {
return err
}
err = s.SQLStore.UpdateDataSource(ctx, cmd)
if err != nil {
return err
}
if query.Result.Name != cmd.Name {
err = s.SecretsStore.Rename(ctx, cmd.OrgId, query.Result.Name, secretType, cmd.Name)
if err != nil {
return err
}
}
return s.SecretsStore.Set(ctx, cmd.OrgId, cmd.Name, secretType, string(secret))
}
func (s *Service) GetDefaultDataSource(ctx context.Context, query *models.GetDefaultDataSourceQuery) error {
return s.SQLStore.GetDefaultDataSource(ctx, query)
}
func (s *Service) GetHTTPClient(ds *models.DataSource, provider httpclient.Provider) (*http.Client, error) {
transport, err := s.GetHTTPTransport(ds, provider)
func (s *Service) GetHTTPClient(ctx context.Context, ds *models.DataSource, provider httpclient.Provider) (*http.Client, error) {
transport, err := s.GetHTTPTransport(ctx, ds, provider)
if err != nil {
return nil, err
}
@@ -213,7 +237,7 @@ func (s *Service) GetHTTPClient(ds *models.DataSource, provider httpclient.Provi
}, nil
}
func (s *Service) GetHTTPTransport(ds *models.DataSource, provider httpclient.Provider,
func (s *Service) GetHTTPTransport(ctx context.Context, ds *models.DataSource, provider httpclient.Provider,
customMiddlewares ...sdkhttpclient.Middleware) (http.RoundTripper, error) {
s.ptc.Lock()
defer s.ptc.Unlock()
@@ -222,7 +246,7 @@ func (s *Service) GetHTTPTransport(ds *models.DataSource, provider httpclient.Pr
return t.roundTripper, nil
}
opts, err := s.httpClientOptions(ds)
opts, err := s.httpClientOptions(ctx, ds)
if err != nil {
return nil, err
}
@@ -242,58 +266,84 @@ func (s *Service) GetHTTPTransport(ds *models.DataSource, provider httpclient.Pr
return rt, nil
}
func (s *Service) GetTLSConfig(ds *models.DataSource, httpClientProvider httpclient.Provider) (*tls.Config, error) {
opts, err := s.httpClientOptions(ds)
func (s *Service) GetTLSConfig(ctx context.Context, ds *models.DataSource, httpClientProvider httpclient.Provider) (*tls.Config, error) {
opts, err := s.httpClientOptions(ctx, ds)
if err != nil {
return nil, err
}
return httpClientProvider.GetTLSConfig(*opts)
}
func (s *Service) DecryptedValues(ds *models.DataSource) map[string]string {
s.dsDecryptionCache.Lock()
defer s.dsDecryptionCache.Unlock()
if item, present := s.dsDecryptionCache.cache[ds.Id]; present && ds.Updated.Equal(item.updated) {
return item.json
}
json, err := s.SecretsService.DecryptJsonData(context.Background(), ds.SecureJsonData)
func (s *Service) DecryptedValues(ctx context.Context, ds *models.DataSource) (map[string]string, error) {
decryptedValues := make(map[string]string)
secret, exist, err := s.SecretsStore.Get(ctx, ds.OrgId, ds.Name, secretType)
if err != nil {
return map[string]string{}
return nil, err
}
s.dsDecryptionCache.cache[ds.Id] = cachedDecryptedJSON{
updated: ds.Updated,
json: json,
if exist {
err := json.Unmarshal([]byte(secret), &decryptedValues)
if err != nil {
return nil, err
}
} else if len(ds.SecureJsonData) > 0 {
decryptedValues, err = s.MigrateSecrets(ctx, ds)
if err != nil {
return nil, err
}
}
return json
return decryptedValues, nil
}
func (s *Service) DecryptedValue(ds *models.DataSource, key string) (string, bool) {
value, exists := s.DecryptedValues(ds)[key]
return value, exists
}
func (s *Service) DecryptedBasicAuthPassword(ds *models.DataSource) string {
if value, ok := s.DecryptedValue(ds, "basicAuthPassword"); ok {
return value
func (s *Service) MigrateSecrets(ctx context.Context, ds *models.DataSource) (map[string]string, error) {
secureJsonData, err := s.SecretsService.DecryptJsonData(ctx, ds.SecureJsonData)
if err != nil {
return nil, err
}
return ds.BasicAuthPassword
}
func (s *Service) DecryptedPassword(ds *models.DataSource) string {
if value, ok := s.DecryptedValue(ds, "password"); ok {
return value
jsonData, err := json.Marshal(secureJsonData)
if err != nil {
return nil, err
}
return ds.Password
err = s.SecretsStore.Set(ctx, ds.OrgId, ds.Name, secretType, string(jsonData))
return secureJsonData, err
}
func (s *Service) httpClientOptions(ds *models.DataSource) (*sdkhttpclient.Options, error) {
tlsOptions := s.dsTLSOptions(ds)
func (s *Service) DecryptedValue(ctx context.Context, ds *models.DataSource, key string) (string, bool, error) {
values, err := s.DecryptedValues(ctx, ds)
if err != nil {
return "", false, err
}
value, exists := values[key]
return value, exists, nil
}
func (s *Service) DecryptedBasicAuthPassword(ctx context.Context, ds *models.DataSource) (string, error) {
value, ok, err := s.DecryptedValue(ctx, ds, "basicAuthPassword")
if ok {
return value, nil
}
return ds.BasicAuthPassword, err
}
func (s *Service) DecryptedPassword(ctx context.Context, ds *models.DataSource) (string, error) {
value, ok, err := s.DecryptedValue(ctx, ds, "password")
if ok {
return value, nil
}
return ds.Password, err
}
func (s *Service) httpClientOptions(ctx context.Context, ds *models.DataSource) (*sdkhttpclient.Options, error) {
tlsOptions, err := s.dsTLSOptions(ctx, ds)
if err != nil {
return nil, err
}
timeouts := &sdkhttpclient.TimeoutOptions{
Timeout: s.getTimeout(ds),
DialTimeout: sdkhttpclient.DefaultTimeoutOptions.DialTimeout,
@@ -305,9 +355,15 @@ func (s *Service) httpClientOptions(ds *models.DataSource) (*sdkhttpclient.Optio
MaxIdleConnsPerHost: sdkhttpclient.DefaultTimeoutOptions.MaxIdleConnsPerHost,
IdleConnTimeout: sdkhttpclient.DefaultTimeoutOptions.IdleConnTimeout,
}
decryptedValues, err := s.DecryptedValues(ctx, ds)
if err != nil {
return nil, err
}
opts := &sdkhttpclient.Options{
Timeouts: timeouts,
Headers: s.getCustomHeaders(ds.JsonData, s.DecryptedValues(ds)),
Headers: s.getCustomHeaders(ds.JsonData, decryptedValues),
Labels: map[string]string{
"datasource_name": ds.Name,
"datasource_uid": ds.Uid,
@@ -318,22 +374,30 @@ func (s *Service) httpClientOptions(ds *models.DataSource) (*sdkhttpclient.Optio
if ds.JsonData != nil {
opts.CustomOptions = ds.JsonData.MustMap()
}
if ds.BasicAuth {
password, err := s.DecryptedBasicAuthPassword(ctx, ds)
if err != nil {
return opts, err
}
opts.BasicAuth = &sdkhttpclient.BasicAuthOptions{
User: ds.BasicAuthUser,
Password: s.DecryptedBasicAuthPassword(ds),
Password: password,
}
} else if ds.User != "" {
password, err := s.DecryptedPassword(ctx, ds)
if err != nil {
return opts, err
}
opts.BasicAuth = &sdkhttpclient.BasicAuthOptions{
User: ds.User,
Password: s.DecryptedPassword(ds),
Password: password,
}
}
// Azure authentication
if ds.JsonData != nil && s.features.IsEnabled(featuremgmt.FlagHttpclientproviderAzureAuth) {
credentials, err := azcredentials.FromDatasourceData(ds.JsonData.MustMap(), s.DecryptedValues(ds))
credentials, err := azcredentials.FromDatasourceData(ds.JsonData.MustMap(), decryptedValues)
if err != nil {
err = fmt.Errorf("invalid Azure credentials: %s", err)
return nil, err
@@ -369,19 +433,27 @@ func (s *Service) httpClientOptions(ds *models.DataSource) (*sdkhttpclient.Optio
Profile: ds.JsonData.Get("sigV4Profile").MustString(),
}
if val, exists := s.DecryptedValue(ds, "sigV4AccessKey"); exists {
opts.SigV4.AccessKey = val
if val, exists, err := s.DecryptedValue(ctx, ds, "sigV4AccessKey"); err == nil {
if exists {
opts.SigV4.AccessKey = val
}
} else {
return opts, err
}
if val, exists := s.DecryptedValue(ds, "sigV4SecretKey"); exists {
opts.SigV4.SecretKey = val
if val, exists, err := s.DecryptedValue(ctx, ds, "sigV4SecretKey"); err == nil {
if exists {
opts.SigV4.SecretKey = val
}
} else {
return opts, err
}
}
return opts, nil
}
func (s *Service) dsTLSOptions(ds *models.DataSource) sdkhttpclient.TLSOptions {
func (s *Service) dsTLSOptions(ctx context.Context, ds *models.DataSource) (sdkhttpclient.TLSOptions, error) {
var tlsSkipVerify, tlsClientAuth, tlsAuthWithCACert bool
var serverName string
@@ -399,22 +471,35 @@ func (s *Service) dsTLSOptions(ds *models.DataSource) sdkhttpclient.TLSOptions {
if tlsClientAuth || tlsAuthWithCACert {
if tlsAuthWithCACert {
if val, exists := s.DecryptedValue(ds, "tlsCACert"); exists && len(val) > 0 {
opts.CACertificate = val
if val, exists, err := s.DecryptedValue(ctx, ds, "tlsCACert"); err == nil {
if exists && len(val) > 0 {
opts.CACertificate = val
}
} else {
return opts, err
}
}
if tlsClientAuth {
if val, exists := s.DecryptedValue(ds, "tlsClientCert"); exists && len(val) > 0 {
opts.ClientCertificate = val
if val, exists, err := s.DecryptedValue(ctx, ds, "tlsClientCert"); err == nil {
fmt.Print("\n\n\n\n", val, exists, err, "\n\n\n\n")
if exists && len(val) > 0 {
opts.ClientCertificate = val
}
} else {
return opts, err
}
if val, exists := s.DecryptedValue(ds, "tlsClientKey"); exists && len(val) > 0 {
opts.ClientKey = val
if val, exists, err := s.DecryptedValue(ctx, ds, "tlsClientKey"); err == nil {
if exists && len(val) > 0 {
opts.ClientKey = val
}
} else {
return opts, err
}
}
}
return opts
return opts, nil
}
func (s *Service) getTimeout(ds *models.DataSource) time.Duration {
@@ -2,6 +2,7 @@ package service
import (
"context"
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
@@ -10,6 +11,8 @@ import (
"github.com/grafana/grafana-azure-sdk-go/azsettings"
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/services/secrets"
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/httpclient"
@@ -17,59 +20,13 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/secrets/database"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/secrets/kvstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestService(t *testing.T) {
cfg := &setting.Cfg{}
sqlStore := sqlstore.InitTestDB(t)
origSecret := setting.SecretKey
setting.SecretKey = "datasources_service_test"
t.Cleanup(func() {
setting.SecretKey = origSecret
})
secretsService := secretsManager.SetupTestService(t, database.ProvideSecretsStore(sqlStore))
s := ProvideService(sqlStore, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
var ds *models.DataSource
t.Run("create datasource should encrypt the secure json data", func(t *testing.T) {
ctx := context.Background()
sjd := map[string]string{"password": "12345"}
cmd := models.AddDataSourceCommand{SecureJsonData: sjd}
err := s.AddDataSource(ctx, &cmd)
require.NoError(t, err)
ds = cmd.Result
decrypted, err := s.SecretsService.DecryptJsonData(ctx, ds.SecureJsonData)
require.NoError(t, err)
require.Equal(t, sjd, decrypted)
})
t.Run("update datasource should encrypt the secure json data", func(t *testing.T) {
ctx := context.Background()
sjd := map[string]string{"password": "678910"}
cmd := models.UpdateDataSourceCommand{Id: ds.Id, OrgId: ds.OrgId, SecureJsonData: sjd}
err := s.UpdateDataSource(ctx, &cmd)
require.NoError(t, err)
decrypted, err := s.SecretsService.DecryptJsonData(ctx, cmd.Result.SecureJsonData)
require.NoError(t, err)
require.Equal(t, sjd, decrypted)
})
}
type dataSourceMockRetriever struct {
res []*models.DataSource
}
@@ -237,15 +194,16 @@ func TestService_GetHttpTransport(t *testing.T) {
Type: "Kubernetes",
}
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
rt1, err := dsService.GetHTTPTransport(&ds, provider)
rt1, err := dsService.GetHTTPTransport(context.Background(), &ds, provider)
require.NoError(t, err)
require.NotNil(t, rt1)
tr1 := configuredTransport
rt2, err := dsService.GetHTTPTransport(&ds, provider)
rt2, err := dsService.GetHTTPTransport(context.Background(), &ds, provider)
require.NoError(t, err)
require.NotNil(t, rt2)
tr2 := configuredTransport
@@ -267,24 +225,22 @@ func TestService_GetHttpTransport(t *testing.T) {
setting.SecretKey = "password"
json := simplejson.New()
json.Set("tlsAuthWithCACert", true)
sjson := simplejson.New()
sjson.Set("tlsAuthWithCACert", true)
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
tlsCaCert, err := secretsService.Encrypt(context.Background(), []byte(caCert), secrets.WithoutScope())
require.NoError(t, err)
dsService := ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
ds := models.DataSource{
Id: 1,
Url: "http://k8s:8001",
Type: "Kubernetes",
SecureJsonData: map[string][]byte{"tlsCACert": tlsCaCert},
SecureJsonData: map[string][]byte{"tlsCACert": []byte(caCert)},
Updated: time.Now().Add(-2 * time.Minute),
}
rt1, err := dsService.GetHTTPTransport(&ds, provider)
rt1, err := dsService.GetHTTPTransport(context.Background(), &ds, provider)
require.NotNil(t, rt1)
require.NoError(t, err)
@@ -298,7 +254,7 @@ func TestService_GetHttpTransport(t *testing.T) {
ds.SecureJsonData = map[string][]byte{}
ds.Updated = time.Now()
rt2, err := dsService.GetHTTPTransport(&ds, provider)
rt2, err := dsService.GetHTTPTransport(context.Background(), &ds, provider)
require.NoError(t, err)
require.NotNil(t, rt2)
tr2 := configuredTransport
@@ -317,30 +273,32 @@ func TestService_GetHttpTransport(t *testing.T) {
setting.SecretKey = "password"
json := simplejson.New()
json.Set("tlsAuth", true)
sjson := simplejson.New()
sjson.Set("tlsAuth", true)
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
tlsClientCert, err := secretsService.Encrypt(context.Background(), []byte(clientCert), secrets.WithoutScope())
require.NoError(t, err)
tlsClientKey, err := secretsService.Encrypt(context.Background(), []byte(clientKey), secrets.WithoutScope())
require.NoError(t, err)
dsService := ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
ds := models.DataSource{
Id: 1,
OrgId: 1,
Name: "kubernetes",
Url: "http://k8s:8001",
Type: "Kubernetes",
JsonData: json,
SecureJsonData: map[string][]byte{
"tlsClientCert": tlsClientCert,
"tlsClientKey": tlsClientKey,
},
JsonData: sjson,
}
rt, err := dsService.GetHTTPTransport(&ds, provider)
secureJsonData, err := json.Marshal(map[string]string{
"tlsClientCert": clientCert,
"tlsClientKey": clientKey,
})
require.NoError(t, err)
err = secretsStore.Set(context.Background(), ds.OrgId, ds.Name, secretType, string(secureJsonData))
require.NoError(t, err)
rt, err := dsService.GetHTTPTransport(context.Background(), &ds, provider)
require.NoError(t, err)
require.NotNil(t, rt)
tr := configuredTransport
@@ -359,27 +317,32 @@ func TestService_GetHttpTransport(t *testing.T) {
setting.SecretKey = "password"
json := simplejson.New()
json.Set("tlsAuthWithCACert", true)
json.Set("serverName", "server-name")
sjson := simplejson.New()
sjson.Set("tlsAuthWithCACert", true)
sjson.Set("serverName", "server-name")
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
tlsCaCert, err := secretsService.Encrypt(context.Background(), []byte(caCert), secrets.WithoutScope())
require.NoError(t, err)
dsService := ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
ds := models.DataSource{
Id: 1,
OrgId: 1,
Name: "kubernetes",
Url: "http://k8s:8001",
Type: "Kubernetes",
JsonData: json,
SecureJsonData: map[string][]byte{
"tlsCACert": tlsCaCert,
},
JsonData: sjson,
}
rt, err := dsService.GetHTTPTransport(&ds, provider)
secureJsonData, err := json.Marshal(map[string]string{
"tlsCACert": caCert,
})
require.NoError(t, err)
err = secretsStore.Set(context.Background(), ds.OrgId, ds.Name, secretType, string(secureJsonData))
require.NoError(t, err)
rt, err := dsService.GetHTTPTransport(context.Background(), &ds, provider)
require.NoError(t, err)
require.NotNil(t, rt)
tr := configuredTransport
@@ -397,25 +360,26 @@ func TestService_GetHttpTransport(t *testing.T) {
},
})
json := simplejson.New()
json.Set("tlsSkipVerify", true)
sjson := simplejson.New()
sjson.Set("tlsSkipVerify", true)
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
ds := models.DataSource{
Id: 1,
Url: "http://k8s:8001",
Type: "Kubernetes",
JsonData: json,
JsonData: sjson,
}
rt1, err := dsService.GetHTTPTransport(&ds, provider)
rt1, err := dsService.GetHTTPTransport(context.Background(), &ds, provider)
require.NoError(t, err)
require.NotNil(t, rt1)
tr1 := configuredTransport
rt2, err := dsService.GetHTTPTransport(&ds, provider)
rt2, err := dsService.GetHTTPTransport(context.Background(), &ds, provider)
require.NoError(t, err)
require.NotNil(t, rt2)
tr2 := configuredTransport
@@ -427,25 +391,32 @@ func TestService_GetHttpTransport(t *testing.T) {
t.Run("Should set custom headers if configured in JsonData", func(t *testing.T) {
provider := httpclient.NewProvider()
json := simplejson.NewFromAny(map[string]interface{}{
sjson := simplejson.NewFromAny(map[string]interface{}{
"httpHeaderName1": "Authorization",
})
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
encryptedData, err := secretsService.Encrypt(context.Background(), []byte(`Bearer xf5yhfkpsnmgo`), secrets.WithoutScope())
require.NoError(t, err)
dsService := ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
ds := models.DataSource{
Id: 1,
Url: "http://k8s:8001",
Type: "Kubernetes",
JsonData: json,
SecureJsonData: map[string][]byte{"httpHeaderValue1": encryptedData},
Id: 1,
OrgId: 1,
Name: "kubernetes",
Url: "http://k8s:8001",
Type: "Kubernetes",
JsonData: sjson,
}
headers := dsService.getCustomHeaders(json, map[string]string{"httpHeaderValue1": "Bearer xf5yhfkpsnmgo"})
secureJsonData, err := json.Marshal(map[string]string{
"httpHeaderValue1": "Bearer xf5yhfkpsnmgo",
})
require.NoError(t, err)
err = secretsStore.Set(context.Background(), ds.OrgId, ds.Name, secretType, string(secureJsonData))
require.NoError(t, err)
headers := dsService.getCustomHeaders(sjson, map[string]string{"httpHeaderValue1": "Bearer xf5yhfkpsnmgo"})
require.Equal(t, "Bearer xf5yhfkpsnmgo", headers["Authorization"])
// 1. Start HTTP test server which checks the request headers
@@ -465,7 +436,7 @@ func TestService_GetHttpTransport(t *testing.T) {
// 2. Get HTTP transport from datasource which uses the test server as backend
ds.Url = backend.URL
rt, err := dsService.GetHTTPTransport(&ds, provider)
rt, err := dsService.GetHTTPTransport(context.Background(), &ds, provider)
require.NoError(t, err)
require.NotNil(t, rt)
@@ -486,21 +457,22 @@ func TestService_GetHttpTransport(t *testing.T) {
t.Run("Should use request timeout if configured in JsonData", func(t *testing.T) {
provider := httpclient.NewProvider()
json := simplejson.NewFromAny(map[string]interface{}{
sjson := simplejson.NewFromAny(map[string]interface{}{
"timeout": 19,
})
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
ds := models.DataSource{
Id: 1,
Url: "http://k8s:8001",
Type: "Kubernetes",
JsonData: json,
JsonData: sjson,
}
client, err := dsService.GetHTTPClient(&ds, provider)
client, err := dsService.GetHTTPClient(context.Background(), &ds, provider)
require.NoError(t, err)
require.NotNil(t, client)
require.Equal(t, 19*time.Second, client.Timeout)
@@ -520,18 +492,19 @@ func TestService_GetHttpTransport(t *testing.T) {
setting.SigV4AuthEnabled = origSigV4Enabled
})
json, err := simplejson.NewJson([]byte(`{ "sigV4Auth": true }`))
sjson, err := simplejson.NewJson([]byte(`{ "sigV4Auth": true }`))
require.NoError(t, err)
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
ds := models.DataSource{
Type: models.DS_ES,
JsonData: json,
JsonData: sjson,
}
_, err = dsService.GetHTTPTransport(&ds, provider)
_, err = dsService.GetHTTPTransport(context.Background(), &ds, provider)
require.NoError(t, err)
require.NotNil(t, configuredOpts)
require.NotNil(t, configuredOpts.SigV4)
@@ -558,8 +531,9 @@ func TestService_getTimeout(t *testing.T) {
{jsonData: simplejson.NewFromAny(map[string]interface{}{"timeout": "2"}), expectedTimeout: 2 * time.Second},
}
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
for _, tc := range testCases {
ds := &models.DataSource{
@@ -569,86 +543,6 @@ func TestService_getTimeout(t *testing.T) {
}
}
func TestService_DecryptedValue(t *testing.T) {
cfg := &setting.Cfg{}
t.Run("When datasource hasn't been updated, encrypted JSON should be fetched from cache", func(t *testing.T) {
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
encryptedJsonData, err := secretsService.EncryptJsonData(
context.Background(),
map[string]string{
"password": "password",
}, secrets.WithoutScope())
require.NoError(t, err)
ds := models.DataSource{
Id: 1,
Type: models.DS_INFLUXDB_08,
JsonData: simplejson.New(),
User: "user",
SecureJsonData: encryptedJsonData,
}
// Populate cache
password, ok := dsService.DecryptedValue(&ds, "password")
require.True(t, ok)
require.Equal(t, "password", password)
encryptedJsonData, err = secretsService.EncryptJsonData(
context.Background(),
map[string]string{
"password": "",
}, secrets.WithoutScope())
require.NoError(t, err)
ds.SecureJsonData = encryptedJsonData
password, ok = dsService.DecryptedValue(&ds, "password")
require.True(t, ok)
require.Equal(t, "password", password)
})
t.Run("When datasource is updated, encrypted JSON should not be fetched from cache", func(t *testing.T) {
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
encryptedJsonData, err := secretsService.EncryptJsonData(
context.Background(),
map[string]string{
"password": "password",
}, secrets.WithoutScope())
require.NoError(t, err)
ds := models.DataSource{
Id: 1,
Type: models.DS_INFLUXDB_08,
JsonData: simplejson.New(),
User: "user",
SecureJsonData: encryptedJsonData,
}
dsService := ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
// Populate cache
password, ok := dsService.DecryptedValue(&ds, "password")
require.True(t, ok)
require.Equal(t, "password", password)
ds.SecureJsonData, err = secretsService.EncryptJsonData(
context.Background(),
map[string]string{
"password": "",
}, secrets.WithoutScope())
ds.Updated = time.Now()
require.NoError(t, err)
password, ok = dsService.DecryptedValue(&ds, "password")
require.True(t, ok)
require.Empty(t, password)
})
}
func TestService_HTTPClientOptions(t *testing.T) {
cfg := &setting.Cfg{
Azure: &azsettings.AzureSettings{},
@@ -678,10 +572,11 @@ func TestService_HTTPClientOptions(t *testing.T) {
"azureEndpointResourceId": "https://api.example.com/abd5c4ce-ca73-41e9-9cb2-bed39aa2adb5",
})
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(nil, secretsService, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock())
dsService := ProvideService(nil, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock())
opts, err := dsService.httpClientOptions(&ds)
opts, err := dsService.httpClientOptions(context.Background(), &ds)
require.NoError(t, err)
require.NotNil(t, opts.Middlewares)
@@ -695,10 +590,11 @@ func TestService_HTTPClientOptions(t *testing.T) {
"httpMethod": "POST",
})
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(nil, secretsService, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock())
dsService := ProvideService(nil, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock())
opts, err := dsService.httpClientOptions(&ds)
opts, err := dsService.httpClientOptions(context.Background(), &ds)
require.NoError(t, err)
if opts.Middlewares != nil {
@@ -714,10 +610,11 @@ func TestService_HTTPClientOptions(t *testing.T) {
"azureCredentials": "invalid",
})
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(nil, secretsService, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock())
dsService := ProvideService(nil, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock())
_, err := dsService.httpClientOptions(&ds)
_, err := dsService.httpClientOptions(context.Background(), &ds)
assert.Error(t, err)
})
@@ -732,10 +629,11 @@ func TestService_HTTPClientOptions(t *testing.T) {
"azureEndpointResourceId": "https://api.example.com/abd5c4ce-ca73-41e9-9cb2-bed39aa2adb5",
})
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(nil, secretsService, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock())
dsService := ProvideService(nil, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock())
opts, err := dsService.httpClientOptions(&ds)
opts, err := dsService.httpClientOptions(context.Background(), &ds)
require.NoError(t, err)
require.NotNil(t, opts.Middlewares)
@@ -750,10 +648,11 @@ func TestService_HTTPClientOptions(t *testing.T) {
"azureEndpointResourceId": "https://api.example.com/abd5c4ce-ca73-41e9-9cb2-bed39aa2adb5",
})
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(nil, secretsService, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock())
dsService := ProvideService(nil, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock())
opts, err := dsService.httpClientOptions(&ds)
opts, err := dsService.httpClientOptions(context.Background(), &ds)
require.NoError(t, err)
if opts.Middlewares != nil {
@@ -772,10 +671,11 @@ func TestService_HTTPClientOptions(t *testing.T) {
"azureEndpointResourceId": "invalid",
})
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(nil, secretsService, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock())
dsService := ProvideService(nil, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock())
_, err := dsService.httpClientOptions(&ds)
_, err := dsService.httpClientOptions(context.Background(), &ds)
assert.Error(t, err)
})
})
@@ -792,10 +692,11 @@ func TestService_HTTPClientOptions(t *testing.T) {
"azureEndpointResourceId": "https://api.example.com/abd5c4ce-ca73-41e9-9cb2-bed39aa2adb5",
})
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
opts, err := dsService.httpClientOptions(&ds)
opts, err := dsService.httpClientOptions(context.Background(), &ds)
require.NoError(t, err)
if opts.Middlewares != nil {
@@ -806,6 +707,59 @@ func TestService_HTTPClientOptions(t *testing.T) {
})
}
func TestService_GetDecryptedValues(t *testing.T) {
t.Run("should migrate and retrieve values from secure json data", func(t *testing.T) {
ds := &models.DataSource{
Id: 1,
Url: "https://api.example.com",
Type: "prometheus",
}
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(nil, secretsService, secretsStore, nil, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
jsonData := map[string]string{
"password": "securePassword",
}
secureJsonData, err := dsService.SecretsService.EncryptJsonData(context.Background(), jsonData, secrets.WithoutScope())
require.NoError(t, err)
ds.SecureJsonData = secureJsonData
values, err := dsService.DecryptedValues(context.Background(), ds)
require.NoError(t, err)
require.Equal(t, jsonData, values)
})
t.Run("should retrieve values from secret store", func(t *testing.T) {
ds := &models.DataSource{
Id: 1,
Url: "https://api.example.com",
Type: "prometheus",
}
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(nil, secretsService, secretsStore, nil, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
jsonData := map[string]string{
"password": "securePassword",
}
jsonString, err := json.Marshal(jsonData)
require.NoError(t, err)
err = secretsStore.Set(context.Background(), ds.OrgId, ds.Name, secretType, string(jsonString))
require.NoError(t, err)
values, err := dsService.DecryptedValues(context.Background(), ds)
require.NoError(t, err)
require.Equal(t, jsonData, values)
})
}
const caCert string = `-----BEGIN CERTIFICATE-----
MIIDATCCAemgAwIBAgIJAMQ5hC3CPDTeMA0GCSqGSIb3DQEBCwUAMBcxFTATBgNV
BAMMDGNhLWs4cy1zdGhsbTAeFw0xNjEwMjcwODQyMjdaFw00NDAzMTQwODQyMjda
+103
View File
@@ -0,0 +1,103 @@
package export
import (
"errors"
"fmt"
"math"
"math/rand"
"sync"
"time"
"github.com/grafana/grafana/pkg/infra/log"
)
var _ Job = new(dummyExportJob)
type dummyExportJob struct {
logger log.Logger
statusMu sync.Mutex
status ExportStatus
cfg ExportConfig
broadcaster statusBroadcaster
}
func startDummyExportJob(cfg ExportConfig, broadcaster statusBroadcaster) (Job, error) {
if cfg.Format != "git" {
return nil, errors.New("only git format is supported")
}
job := &dummyExportJob{
logger: log.New("dummy_export_job"),
cfg: cfg,
broadcaster: broadcaster,
status: ExportStatus{
Running: true,
Target: "git export",
Started: time.Now().UnixMilli(),
Count: int64(math.Round(10 + rand.Float64()*20)),
Current: 0,
},
}
broadcaster(job.status)
go job.start()
return job, nil
}
func (e *dummyExportJob) start() {
defer func() {
e.logger.Info("Finished dummy export job")
e.statusMu.Lock()
defer e.statusMu.Unlock()
s := e.status
if err := recover(); err != nil {
e.logger.Error("export panic", "error", err)
s.Status = fmt.Sprintf("ERROR: %v", err)
}
// Make sure it finishes OK
if s.Finished < 10 {
s.Finished = time.Now().UnixMilli()
}
s.Running = false
if s.Status == "" {
s.Status = "done"
}
e.status = s
e.broadcaster(s)
}()
e.logger.Info("Starting dummy export job")
ticker := time.NewTicker(1 * time.Second)
for t := range ticker.C {
e.statusMu.Lock()
e.status.Changed = t.UnixMilli()
e.status.Current++
e.status.Last = fmt.Sprintf("ITEM: %d", e.status.Current)
e.statusMu.Unlock()
// Wait till we are done
shouldStop := e.status.Current >= e.status.Count
e.broadcaster(e.status)
if shouldStop {
break
}
}
}
func (e *dummyExportJob) getStatus() ExportStatus {
e.statusMu.Lock()
defer e.statusMu.Unlock()
return e.status
}
func (e *dummyExportJob) getConfig() ExportConfig {
e.statusMu.Lock()
defer e.statusMu.Unlock()
return e.cfg
}
+93
View File
@@ -0,0 +1,93 @@
package export
import (
"encoding/json"
"net/http"
"sync"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/live"
"github.com/grafana/grafana/pkg/services/sqlstore"
)
type ExportService interface {
// List folder contents
HandleGetStatus(c *models.ReqContext) response.Response
// Read raw file contents out of the store
HandleRequestExport(c *models.ReqContext) response.Response
}
type StandardExport struct {
logger log.Logger
sql *sqlstore.SQLStore
glive *live.GrafanaLive
mutex sync.Mutex
// updated with mutex
exportJob Job
}
func ProvideService(sql *sqlstore.SQLStore, features featuremgmt.FeatureToggles, gl *live.GrafanaLive) ExportService {
if !features.IsEnabled(featuremgmt.FlagExport) {
return &StubExport{}
}
return &StandardExport{
sql: sql,
glive: gl,
logger: log.New("export_service"),
exportJob: &stoppedJob{},
}
}
func (ex *StandardExport) HandleGetStatus(c *models.ReqContext) response.Response {
ex.mutex.Lock()
defer ex.mutex.Unlock()
return response.JSON(http.StatusOK, ex.exportJob.getStatus())
}
func (ex *StandardExport) HandleRequestExport(c *models.ReqContext) response.Response {
var cfg ExportConfig
err := json.NewDecoder(c.Req.Body).Decode(&cfg)
if err != nil {
return response.Error(http.StatusBadRequest, "unable to read config", err)
}
ex.mutex.Lock()
defer ex.mutex.Unlock()
status := ex.exportJob.getStatus()
if status.Running {
ex.logger.Error("export already running")
return response.Error(http.StatusLocked, "export already running", nil)
}
job, err := startDummyExportJob(cfg, func(s ExportStatus) {
ex.broadcastStatus(c.OrgId, s)
})
if err != nil {
ex.logger.Error("failed to start export job", "err", err)
return response.Error(http.StatusBadRequest, "failed to start export job", err)
}
ex.exportJob = job
return response.JSON(http.StatusOK, ex.exportJob.getStatus())
}
func (ex *StandardExport) broadcastStatus(orgID int64, s ExportStatus) {
msg, err := json.Marshal(s)
if err != nil {
ex.logger.Warn("Error making message", "err", err)
return
}
err = ex.glive.Publish(orgID, "grafana/broadcast/export", msg)
if err != nil {
ex.logger.Warn("Error Publish message", "err", err)
return
}
}
+19
View File
@@ -0,0 +1,19 @@
package export
import "time"
var _ Job = new(stoppedJob)
type stoppedJob struct {
}
func (e *stoppedJob) getStatus() ExportStatus {
return ExportStatus{
Running: false,
Changed: time.Now().UnixMilli(),
}
}
func (e *stoppedJob) getConfig() ExportConfig {
return ExportConfig{}
}
+20
View File
@@ -0,0 +1,20 @@
package export
import (
"net/http"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
)
var _ ExportService = new(StubExport)
type StubExport struct{}
func (ex *StubExport) HandleGetStatus(c *models.ReqContext) response.Response {
return response.Error(http.StatusForbidden, "feature not enabled", nil)
}
func (ex *StubExport) HandleRequestExport(c *models.ReqContext) response.Response {
return response.Error(http.StatusForbidden, "feature not enabled", nil)
}
+36
View File
@@ -0,0 +1,36 @@
package export
// Export status. Only one running at a time
type ExportStatus struct {
Running bool `json:"running"`
Target string `json:"target"` // description of where it is going (no secrets)
Started int64 `json:"started,omitempty"`
Finished int64 `json:"finished,omitempty"`
Changed int64 `json:"update,omitempty"`
Count int64 `json:"count,omitempty"`
Current int64 `json:"current,omitempty"`
Last string `json:"last,omitempty"`
Status string `json:"status"` // ERROR, SUCCESS, ETC
}
// Basic export config (for now)
type ExportConfig struct {
Format string `json:"format"`
Git GitExportConfig `json:"git"`
}
type GitExportConfig struct {
// General folder is either at the root or as a subfolder
GeneralAtRoot bool `json:"generalAtRoot"`
// Keeping all history is nice, but much slower
ExcludeHistory bool `json:"excludeHistory"`
}
type Job interface {
getStatus() ExportStatus
getConfig() ExportConfig
}
// Will broadcast the live status
type statusBroadcaster func(s ExportStatus)
+21 -8
View File
@@ -39,14 +39,10 @@ var (
State: FeatureStateAlpha,
},
{
Name: "dashboardPreviewsScheduler",
Description: "Schedule automatic updates to dashboard previews",
State: FeatureStateAlpha,
},
{
Name: "dashboardPreviewsAdmin",
Description: "Manage the dashboard previews crawler process from the UI",
State: FeatureStateAlpha,
Name: "dashboardPreviewsAdmin",
Description: "Manage the dashboard previews crawler process from the UI",
State: FeatureStateAlpha,
RequiresDevMode: true,
},
{
Name: "live-config",
@@ -194,6 +190,12 @@ var (
Description: "Provisioning-friendly routes for alerting",
State: FeatureStateAlpha,
},
{
Name: "export",
Description: "Export grafana instance (to git, etc)",
State: FeatureStateAlpha,
RequiresDevMode: true,
},
{
Name: "storageLocalUpload",
Description: "allow uploads to local storage",
@@ -213,6 +215,12 @@ var (
State: FeatureStateBeta,
FrontendOnly: true,
},
{
Name: "tracing",
Description: "Adds trace ID to error notifications",
State: FeatureStateAlpha,
FrontendOnly: true,
},
{
Name: "persistNotifications",
Description: "PoC Notifications page",
@@ -224,5 +232,10 @@ var (
Description: "Enable command palette",
State: FeatureStateAlpha,
},
{
Name: "savedItems",
Description: "Enable Saved Items in the navbar.",
State: FeatureStateAlpha,
},
}
)
+12 -4
View File
@@ -31,10 +31,6 @@ const (
// Create and show thumbnails for dashboard search results
FlagDashboardPreviews = "dashboardPreviews"
// FlagDashboardPreviewsScheduler
// Schedule automatic updates to dashboard previews
FlagDashboardPreviewsScheduler = "dashboardPreviewsScheduler"
// FlagDashboardPreviewsAdmin
// Manage the dashboard previews crawler process from the UI
FlagDashboardPreviewsAdmin = "dashboardPreviewsAdmin"
@@ -147,6 +143,10 @@ const (
// Provisioning-friendly routes for alerting
FlagAlertProvisioning = "alertProvisioning"
// FlagExport
// Export grafana instance (to git, etc)
FlagExport = "export"
// FlagStorageLocalUpload
// allow uploads to local storage
FlagStorageLocalUpload = "storageLocalUpload"
@@ -159,6 +159,10 @@ const (
// Experimental Explore to Dashboard workflow
FlagExplore2Dashboard = "explore2Dashboard"
// FlagTracing
// Adds trace ID to error notifications
FlagTracing = "tracing"
// FlagPersistNotifications
// PoC Notifications page
FlagPersistNotifications = "persistNotifications"
@@ -166,4 +170,8 @@ const (
// FlagCommandPalette
// Enable command palette
FlagCommandPalette = "commandPalette"
// FlagSavedItems
// Enable Saved Items in the navbar.
FlagSavedItems = "savedItems"
)
+1 -1
View File
@@ -12,7 +12,7 @@ import (
type Provider struct{}
func ProvideService(store *sqlstore.SQLStore, ac accesscontrol.AccessControl, permissionsServices accesscontrol.PermissionsServices, features featuremgmt.FeatureToggles) *Provider {
if features.IsEnabled(featuremgmt.FlagAccesscontrol) {
if !ac.IsDisabled() {
// TODO: Fix this hack, see https://github.com/grafana/grafana-enterprise/issues/2935
InitAcessControlGuardian(store, ac, permissionsServices)
} else {
+8 -4
View File
@@ -46,9 +46,13 @@ Scopes must have an order to ensure consistency and ease of search, this helps u
## Grafana Alerting - main / unreleased
- [CHANGE] Prometheus Compatible API: Use float-like values for `api/prometheus/grafana/api/v1/alerts` and `api/prometheus/grafana/api/v1/rules` instead of the evaluation string #47216
- [BUGFIX] (Legacy) Templates: Parse notification templates using all the matches of the alert rule when going from `Alerting` to `OK` in legacy alerting #47355
- [BUGFIX] Scheduler: Fix state manager to support OK option of `AlertRule.ExecErrState` #47670
- [ENHANCEMENT] Templates: Enable the use of classic condition values in templates #46971
- [ENHANCEMENT] Scheduler: ticker expose new metrics `grafana_alerting_ticker_last_consumed_tick_timestamp_seconds`, `grafana_alerting_ticker_next_tick_timestamp_seconds`, `grafana_alerting_ticker_interval_seconds` #47828
- [CHANGE] Notification URL points to alert view page instead of alert edit page. #47752
- [FEATURE] Indicate whether routes are provisioned when GETting Alertmanager configuration #47857
- [BUGFIX] (Legacy) Templates: Parse notification templates using all the matches of the alert rule when going from `Alerting` to `OK` in legacy alerting #47355
- [BUGFIX] Scheduler: Fix state manager to support OK option of `AlertRule.ExecErrState` #47670
- [ENHANCEMENT] Templates: Enable the use of classic condition values in templates #46971
- [ENHANCEMENT] Scheduler: Ticker expose new metrics. In legacy, metrics are prefixed with `legacy_` #47828, #48190
- `grafana_alerting_ticker_last_consumed_tick_timestamp_seconds`
- `grafana_alerting_ticker_next_tick_timestamp_seconds`
- `grafana_alerting_ticker_interval_seconds`
- [ENHANCEMENT] Migration: Migrate each legacy notification channel to its own contact point, use nested routes to reproduce multi-channel alerts #47291
@@ -505,8 +505,7 @@ func createRequestCtxInOrg(org int64) *models.ReqContext {
// setRouteProvenance marks an org's routing tree as provisioned.
func setRouteProvenance(t *testing.T, org int64, ps provisioning.ProvisioningStore) {
t.Helper()
adp := provisioning.ProvenanceOrgAdapter{Inner: &apimodels.Route{}, OrgID: org}
err := ps.SetProvenance(context.Background(), adp, ngmodels.ProvenanceAPI)
err := ps.SetProvenance(context.Background(), &apimodels.Route{}, org, ngmodels.ProvenanceAPI)
require.NoError(t, err)
}
+1 -1
View File
@@ -147,7 +147,7 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *models.ReqContext) response.Res
DashboardUID: dashboardUID,
PanelID: panelID,
}
if err := srv.store.GetOrgAlertRules(c.Req.Context(), &alertRuleQuery); err != nil {
if err := srv.store.ListAlertRules(c.Req.Context(), &alertRuleQuery); err != nil {
ruleResponse.DiscoveryBase.Status = "error"
ruleResponse.DiscoveryBase.Error = fmt.Sprintf("failure getting rules: %s", err.Error())
ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrServer
+22 -22
View File
@@ -55,9 +55,9 @@ func (srv RulerSrv) RouteDeleteAlertRules(c *models.ReqContext) response.Respons
"namespace",
namespace.Title,
}
var ruleGroup *string
var ruleGroup string
if group, ok := web.Params(c.Req)[":Groupname"]; ok {
ruleGroup = &group
ruleGroup = group
loggerCtx = append(loggerCtx, "group", group)
}
logger := srv.log.New(loggerCtx...)
@@ -68,12 +68,12 @@ func (srv RulerSrv) RouteDeleteAlertRules(c *models.ReqContext) response.Respons
var canDelete, cannotDelete []string
err = srv.xactManager.InTransaction(c.Req.Context(), func(ctx context.Context) error {
q := ngmodels.GetAlertRulesQuery{
OrgID: c.SignedInUser.OrgId,
NamespaceUID: namespace.Uid,
RuleGroup: ruleGroup,
q := ngmodels.ListAlertRulesQuery{
OrgID: c.SignedInUser.OrgId,
NamespaceUIDs: []string{namespace.Uid},
RuleGroup: ruleGroup,
}
if err = srv.store.GetAlertRules(ctx, &q); err != nil {
if err = srv.store.ListAlertRules(ctx, &q); err != nil {
return err
}
@@ -128,11 +128,11 @@ func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *models.ReqContext) response.
return toNamespaceErrorResponse(err)
}
q := ngmodels.GetAlertRulesQuery{
OrgID: c.SignedInUser.OrgId,
NamespaceUID: namespace.Uid,
q := ngmodels.ListAlertRulesQuery{
OrgID: c.SignedInUser.OrgId,
NamespaceUIDs: []string{namespace.Uid},
}
if err := srv.store.GetAlertRules(c.Req.Context(), &q); err != nil {
if err := srv.store.ListAlertRules(c.Req.Context(), &q); err != nil {
return ErrResp(http.StatusInternalServerError, err, "failed to update rule group")
}
@@ -178,12 +178,12 @@ func (srv RulerSrv) RouteGetRulegGroupConfig(c *models.ReqContext) response.Resp
}
ruleGroup := web.Params(c.Req)[":Groupname"]
q := ngmodels.GetAlertRulesQuery{
OrgID: c.SignedInUser.OrgId,
NamespaceUID: namespace.Uid,
RuleGroup: &ruleGroup,
q := ngmodels.ListAlertRulesQuery{
OrgID: c.SignedInUser.OrgId,
NamespaceUIDs: []string{namespace.Uid},
RuleGroup: ruleGroup,
}
if err := srv.store.GetAlertRules(c.Req.Context(), &q); err != nil {
if err := srv.store.ListAlertRules(c.Req.Context(), &q); err != nil {
return ErrResp(http.StatusInternalServerError, err, "failed to get group alert rules")
}
@@ -245,7 +245,7 @@ func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response
PanelID: panelID,
}
if err := srv.store.GetOrgAlertRules(c.Req.Context(), &q); err != nil {
if err := srv.store.ListAlertRules(c.Req.Context(), &q); err != nil {
return ErrResp(http.StatusInternalServerError, err, "failed to get alert rules")
}
@@ -493,12 +493,12 @@ func (c *changes) isEmpty() bool {
// calculateChanges calculates the difference between rules in the group in the database and the submitted rules. If a submitted rule has UID it tries to find it in the database (in other groups).
// returns a list of rules that need to be added, updated and deleted. Deleted considered rules in the database that belong to the group but do not exist in the list of submitted rules.
func calculateChanges(ctx context.Context, ruleStore store.RuleStore, orgId int64, namespace *models.Folder, ruleGroupName string, submittedRules []*ngmodels.AlertRule) (*changes, error) {
q := &ngmodels.GetAlertRulesQuery{
OrgID: orgId,
NamespaceUID: namespace.Uid,
RuleGroup: &ruleGroupName,
q := &ngmodels.ListAlertRulesQuery{
OrgID: orgId,
NamespaceUIDs: []string{namespace.Uid},
RuleGroup: ruleGroupName,
}
if err := ruleStore.GetAlertRules(ctx, q); err != nil {
if err := ruleStore.ListAlertRules(ctx, q); err != nil {
return nil, fmt.Errorf("failed to query database for rules in the group %s: %w", ruleGroupName, err)
}
existingGroupRules := q.Result
+1 -1
View File
@@ -232,7 +232,7 @@ func TestCalculateChanges(t *testing.T) {
expectedErr := errors.New("TEST ERROR")
fakeStore.Hook = func(cmd interface{}) error {
switch cmd.(type) {
case models.GetAlertRulesQuery:
case models.ListAlertRulesQuery:
return expectedErr
}
return nil
+6 -5
View File
@@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
acMock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/datasources"
fakes "github.com/grafana/grafana/pkg/services/datasources/fakes"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/eval"
"github.com/grafana/grafana/pkg/services/ngalert/models"
@@ -61,7 +62,7 @@ func TestRouteTestGrafanaRuleConfig(t *testing.T) {
{Action: datasources.ActionQuery, Scope: datasources.ScopeProvider.GetResourceScopeUID(data2.DatasourceUID)},
})
ds := &datasources.FakeCacheService{DataSources: []*models2.DataSource{
ds := &fakes.FakeCacheService{DataSources: []*models2.DataSource{
{Uid: data1.DatasourceUID},
{Uid: data2.DatasourceUID},
}}
@@ -102,7 +103,7 @@ func TestRouteTestGrafanaRuleConfig(t *testing.T) {
t.Run("should require user to be signed in", func(t *testing.T) {
data1 := models.GenerateAlertQuery()
ds := &datasources.FakeCacheService{DataSources: []*models2.DataSource{
ds := &fakes.FakeCacheService{DataSources: []*models2.DataSource{
{Uid: data1.DatasourceUID},
}}
@@ -182,7 +183,7 @@ func TestRouteEvalQueries(t *testing.T) {
{Action: datasources.ActionQuery, Scope: datasources.ScopeProvider.GetResourceScopeUID(data2.DatasourceUID)},
})
ds := &datasources.FakeCacheService{DataSources: []*models2.DataSource{
ds := &fakes.FakeCacheService{DataSources: []*models2.DataSource{
{Uid: data1.DatasourceUID},
{Uid: data2.DatasourceUID},
}}
@@ -226,7 +227,7 @@ func TestRouteEvalQueries(t *testing.T) {
t.Run("should require user to be signed in", func(t *testing.T) {
data1 := models.GenerateAlertQuery()
ds := &datasources.FakeCacheService{DataSources: []*models2.DataSource{
ds := &fakes.FakeCacheService{DataSources: []*models2.DataSource{
{Uid: data1.DatasourceUID},
}}
@@ -265,7 +266,7 @@ func TestRouteEvalQueries(t *testing.T) {
})
}
func createTestingApiSrv(ds *datasources.FakeCacheService, ac *acMock.Mock, evaluator *eval.FakeEvaluator) *TestingApiSrv {
func createTestingApiSrv(ds *fakes.FakeCacheService, ac *acMock.Mock, evaluator *eval.FakeEvaluator) *TestingApiSrv {
if ac == nil {
ac = acMock.New().WithDisabled()
}
+1 -15
View File
@@ -232,6 +232,7 @@ type ListAlertRulesQuery struct {
OrgID int64
NamespaceUIDs []string
ExcludeOrgs []int64
RuleGroup string
// DashboardUID and PanelID are optional and allow filtering rules
// to return just those for a dashboard and panel.
@@ -250,21 +251,6 @@ type ListNamespaceAlertRulesQuery struct {
Result []*AlertRule
}
// GetAlertRulesQuery is the query for listing rule group alert rules
type GetAlertRulesQuery struct {
OrgID int64
// Namespace is the folder slug
NamespaceUID string
RuleGroup *string
// DashboardUID and PanelID are optional and allow filtering rules
// to return just those for a dashboard and panel.
DashboardUID string
PanelID int64
Result []*AlertRule
}
// ListRuleGroupsQuery is the query for listing unique rule groups
// across all organizations
type ListRuleGroupsQuery struct {
@@ -14,11 +14,4 @@ const (
type Provisionable interface {
ResourceType() string
ResourceID() string
ResourceOrgID() int64
}
// ProvisionableInOrg represents a resource that can be provisioned, given external org-related information.
type ProvisionableInOrg interface {
ResourceType() string
ResourceID() string
}
@@ -8,7 +8,6 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
"github.com/grafana/grafana/pkg/services/ngalert/store"
)
@@ -129,11 +128,7 @@ func (moa *MultiOrgAlertmanager) ApplyAlertmanagerConfiguration(ctx context.Cont
func (moa *MultiOrgAlertmanager) mergeProvenance(ctx context.Context, config definitions.GettableApiAlertingConfig, org int64) (definitions.GettableApiAlertingConfig, error) {
if config.Route != nil {
adp := provisioning.ProvenanceOrgAdapter{
Inner: config.Route,
OrgID: org,
}
provenance, err := moa.ProvStore.GetProvenance(ctx, adp)
provenance, err := moa.ProvStore.GetProvenance(ctx, config.Route, org)
if err != nil {
return definitions.GettableApiAlertingConfig{}, err
}
@@ -177,11 +177,7 @@ func (ecp *ContactPointService) CreateContactPoint(ctx context.Context, orgID in
if err != nil {
return err
}
adapter := ProvenanceOrgAdapter{
Inner: &contactPoint,
OrgID: orgID,
}
err = ecp.provenanceStore.SetProvenance(ctx, adapter, provenance)
err = ecp.provenanceStore.SetProvenance(ctx, &contactPoint, orgID, provenance)
if err != nil {
return err
}
@@ -218,10 +214,7 @@ func (ecp *ContactPointService) UpdateContactPoint(ctx context.Context, orgID in
return err
}
// check that provenance is not changed in a invalid way
storedProvenance, err := ecp.provenanceStore.GetProvenance(ctx, ProvenanceOrgAdapter{
Inner: &contactPoint,
OrgID: orgID,
})
storedProvenance, err := ecp.provenanceStore.GetProvenance(ctx, &contactPoint, orgID)
if err != nil {
return err
}
@@ -283,11 +276,7 @@ func (ecp *ContactPointService) UpdateContactPoint(ctx context.Context, orgID in
if err != nil {
return err
}
adapter := ProvenanceOrgAdapter{
Inner: &contactPoint,
OrgID: orgID,
}
err = ecp.provenanceStore.SetProvenance(ctx, adapter, provenance)
err = ecp.provenanceStore.SetProvenance(ctx, &contactPoint, orgID, provenance)
if err != nil {
return err
}
@@ -330,12 +319,10 @@ func (ecp *ContactPointService) DeleteContactPoint(ctx context.Context, orgID in
return err
}
return ecp.xact.InTransaction(ctx, func(ctx context.Context) error {
err := ecp.provenanceStore.DeleteProvenance(ctx, ProvenanceOrgAdapter{
Inner: &apimodels.EmbeddedContactPoint{
UID: uid,
},
OrgID: orgID,
})
target := &apimodels.EmbeddedContactPoint{
UID: uid,
}
err := ecp.provenanceStore.DeleteProvenance(ctx, target, orgID)
if err != nil {
return err
}

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