Merge branch 'main' into iwysiu/47597
This commit is contained in:
+7
-7
@@ -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
|
||||
|
||||
...
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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');
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) })
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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{}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user