Merge branch 'main' of github.com:grafana/grafana into task/45304-integrate-alerts-with-folders

This commit is contained in:
Konrad Lalik
2022-02-21 13:19:00 +01:00
529 changed files with 13989 additions and 6375 deletions
+6 -21
View File
@@ -8,21 +8,9 @@ exports[`no enzyme tests`] = {
"packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx:2315765050": [
[1, 19, 13, "RegExp match", "2409514259"]
],
"packages/grafana-ui/src/components/BigValue/BigValue.test.tsx:261212211": [
[1, 19, 13, "RegExp match", "2409514259"]
],
"packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.test.tsx:2771878691": [
[1, 18, 13, "RegExp match", "2409514259"]
],
"packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.test.tsx:1355456933": [
[1, 31, 13, "RegExp match", "2409514259"]
],
"packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.test.tsx:3194817479": [
[2, 33, 13, "RegExp match", "2409514259"]
],
"packages/grafana-ui/src/components/ConfirmModal/ConfirmModal.test.tsx:3838344574": [
[1, 17, 13, "RegExp match", "2409514259"]
],
"packages/grafana-ui/src/components/FileUpload/FileUpload.test.tsx:3475964456": [
[1, 19, 13, "RegExp match", "2409514259"]
],
@@ -116,7 +104,7 @@ exports[`no enzyme tests`] = {
"packages/jaeger-ui-components/src/TraceTimelineViewer/ReferencesButton.test.js:2288177454": [
[15, 19, 13, "RegExp match", "2409514259"]
],
"packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBar.test.js:2545268168": [
"packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBar.test.js:2127169675": [
[15, 17, 13, "RegExp match", "2409514259"]
],
"packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.test.js:2454947085": [
@@ -197,9 +185,6 @@ exports[`no enzyme tests`] = {
"public/app/core/components/Select/MetricSelect.test.tsx:3409251428": [
[1, 19, 13, "RegExp match", "2409514259"]
],
"public/app/core/specs/OrgSwitcher.test.tsx:848670248": [
[2, 19, 13, "RegExp match", "2409514259"]
],
"public/app/features/alerting/AlertRuleList.test.tsx:1800339390": [
[1, 19, 13, "RegExp match", "2409514259"]
],
@@ -218,7 +203,7 @@ exports[`no enzyme tests`] = {
"public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.test.tsx:4134073823": [
[1, 17, 13, "RegExp match", "2409514259"]
],
"public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx:1141305288": [
"public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx:1044891955": [
[1, 35, 13, "RegExp match", "2409514259"]
],
"public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx:1798654441": [
@@ -290,6 +275,9 @@ exports[`no enzyme tests`] = {
"public/app/features/folders/FolderSettingsPage.test.tsx:1751147194": [
[2, 19, 13, "RegExp match", "2409514259"]
],
"public/app/features/invites/InviteesTable.test.tsx:3077684439": [
[1, 19, 13, "RegExp match", "2409514259"]
],
"public/app/features/org/OrgDetailsPage.test.tsx:2540662821": [
[1, 19, 13, "RegExp match", "2409514259"]
],
@@ -317,13 +305,10 @@ exports[`no enzyme tests`] = {
"public/app/features/teams/TeamSettings.test.tsx:2628968507": [
[1, 19, 13, "RegExp match", "2409514259"]
],
"public/app/features/users/InviteesTable.test.tsx:2271264692": [
[1, 19, 13, "RegExp match", "2409514259"]
],
"public/app/features/users/UsersActionBar.test.tsx:4031641375": [
[1, 19, 13, "RegExp match", "2409514259"]
],
"public/app/features/users/UsersListPage.test.tsx:2523261097": [
"public/app/features/users/UsersListPage.test.tsx:2626906707": [
[1, 19, 13, "RegExp match", "2409514259"]
],
"public/app/features/users/UsersTable.test.tsx:3051231816": [
+2 -1
View File
@@ -6,6 +6,7 @@
load('scripts/drone/pipelines/pr.star', 'pr_pipelines')
load('scripts/drone/pipelines/main.star', 'main_pipelines')
load('scripts/drone/pipelines/docs.star', 'docs_pipelines')
load('scripts/drone/pipelines/release.star', 'release_pipelines', 'publish_image_pipelines', 'publish_artifacts_pipelines', 'publish_npm_pipelines', 'publish_packages_pipeline')
load('scripts/drone/version.star', 'version_branch_pipelines')
load('scripts/drone/pipelines/cron.star', 'cronjobs')
@@ -13,7 +14,7 @@ load('scripts/drone/vault.star', 'secrets')
def main(ctx):
edition = 'oss'
return pr_pipelines(edition=edition) + main_pipelines(edition=edition) + release_pipelines() + \
return pr_pipelines(edition=edition) + docs_pipelines(edition=edition) + main_pipelines(edition=edition) + release_pipelines() + \
publish_image_pipelines('public') + publish_image_pipelines('security') + \
publish_artifacts_pipelines('security') + publish_artifacts_pipelines('public') + \
publish_npm_pipelines('public') + publish_packages_pipeline() + \
+104 -47
View File
@@ -11,7 +11,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -93,6 +93,9 @@ steps:
trigger:
event:
- pull_request
paths:
exclude:
- docs/**
type: docker
volumes:
- host:
@@ -111,7 +114,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -288,20 +291,6 @@ steps:
failure: always
image: grafana/docker-puppeteer:1.0.0
name: test-a11y-frontend
- commands:
- ./scripts/ci-reference-docs-lint.sh ci
depends_on:
- build-frontend
image: grafana/build-container:1.4.9
name: build-frontend-docs
- commands:
- mkdir -p /hugo/content/docs/grafana
- cp -r docs/sources/* /hugo/content/docs/grafana/latest/
- cd /hugo && make prod
depends_on:
- build-frontend-docs
image: grafana/docs-base:latest
name: build-docs-website
- commands:
- ls dist/*.tar.gz*
- cp dist/*.tar.gz* packaging/docker/
@@ -324,6 +313,9 @@ steps:
trigger:
event:
- pull_request
paths:
exclude:
- docs/**
type: docker
volumes:
- host:
@@ -362,7 +354,7 @@ services:
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -399,6 +391,9 @@ steps:
trigger:
event:
- pull_request
paths:
exclude:
- docs/**
type: docker
volumes:
- host:
@@ -413,6 +408,68 @@ volumes:
---
depends_on: []
kind: pipeline
name: pr-docs
node:
type: no-parallel
platform:
arch: amd64
os: linux
services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
- commands:
- echo $DRONE_RUNNER_NAME
image: alpine:3.15
name: identify-runner
- commands:
- make gen-go
- ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER}
- yarn install --immutable
image: grafana/build-container:1.4.9
name: initialize
- commands:
- ./bin/grabpl build-frontend --jobs 8 --no-install-deps --edition oss --build-id
${DRONE_BUILD_NUMBER} --no-pull-enterprise
depends_on:
- initialize
environment:
NODE_OPTIONS: --max_old_space_size=8192
image: grafana/build-container:1.4.9
name: build-frontend
- commands:
- ./scripts/ci-reference-docs-lint.sh ci
depends_on:
- build-frontend
image: grafana/build-container:1.4.9
name: build-frontend-docs
- commands:
- mkdir -p /hugo/content/docs/grafana
- cp -r docs/sources/* /hugo/content/docs/grafana/latest/
- cd /hugo && make prod
depends_on:
- build-frontend-docs
image: grafana/docs-base:latest
name: build-docs-website
trigger:
event:
- pull_request
paths:
include:
- docs/**
- packages/**
type: docker
volumes:
- host:
path: /var/run/docker.sock
name: docker
---
depends_on: []
kind: pipeline
name: main-test
node:
type: no-parallel
@@ -423,7 +480,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -524,7 +581,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -909,7 +966,7 @@ services:
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -977,7 +1034,7 @@ steps:
name: identify-runner
- commands:
- $$ProgressPreference = "SilentlyContinue"
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/windows/grabpl.exe
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/windows/grabpl.exe
-OutFile grabpl.exe
image: grafana/ci-wix:0.1.1
name: initialize
@@ -1060,7 +1117,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -1142,7 +1199,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -1446,7 +1503,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -1567,7 +1624,7 @@ services:
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -1651,7 +1708,7 @@ steps:
name: identify-runner
- commands:
- $$ProgressPreference = "SilentlyContinue"
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/windows/grabpl.exe
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/windows/grabpl.exe
-OutFile grabpl.exe
image: grafana/ci-wix:0.1.1
name: initialize
@@ -1710,7 +1767,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2050,7 +2107,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2223,7 +2280,7 @@ services:
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2351,7 +2408,7 @@ steps:
name: identify-runner
- commands:
- $$ProgressPreference = "SilentlyContinue"
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/windows/grabpl.exe
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/windows/grabpl.exe
-OutFile grabpl.exe
- git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git"
- cd grafana-enterprise
@@ -2426,7 +2483,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2504,7 +2561,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2565,7 +2622,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2644,7 +2701,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2706,7 +2763,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2742,7 +2799,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2789,7 +2846,7 @@ steps:
name: initialize
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2837,7 +2894,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2900,7 +2957,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -3159,7 +3216,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -3273,7 +3330,7 @@ services:
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -3350,7 +3407,7 @@ steps:
name: identify-runner
- commands:
- $$ProgressPreference = "SilentlyContinue"
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/windows/grabpl.exe
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/windows/grabpl.exe
-OutFile grabpl.exe
image: grafana/ci-wix:0.1.1
name: initialize
@@ -3398,7 +3455,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -3731,7 +3788,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -3894,7 +3951,7 @@ services:
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -4012,7 +4069,7 @@ steps:
name: identify-runner
- commands:
- $$ProgressPreference = "SilentlyContinue"
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/windows/grabpl.exe
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/windows/grabpl.exe
-OutFile grabpl.exe
- git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git"
- cd grafana-enterprise
@@ -4209,6 +4266,6 @@ kind: secret
name: gcp_upload_artifacts_key
---
kind: signature
hmac: 559c02d03809e345c15ec6badc083947ac12d79dc851596a317a5bd83ad75941
hmac: 703b9a44a073ab768a774029cee2c3fc7c5fe7b7e01495e18f048bc7d1f007b0
...
+1 -1
View File
@@ -104,7 +104,7 @@ go.sum @grafana/backend-platform
/public/app/features/live/ @grafana/grafana-edge-squad
/public/app/features/explore/ @grafana/observability-experience-squad
/public/app/features/plugins @grafana/plugins-platform-frontend
/public/app/core/components/TransformersUI/spatial @grafana/grafana-edge-squad
/public/app/features/transformers/spatial @grafana/grafana-edge-squad
/public/app/plugins/panel/alertlist @grafana/alerting-squad
/public/app/plugins/panel/barchart @grafana/grafana-bi-squad
/public/app/plugins/panel/heatmap @grafana/grafana-bi-squad
@@ -112,7 +112,7 @@ jobs:
- name: Get link for the Github Action job
id: job
uses: actions/github-script@v5
uses: actions/github-script@v6
with:
script: |
const script = require('./.github/workflows/scripts/pr-get-job-link.js')
@@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v2
- name: 'Download artifact'
uses: actions/github-script@v5
uses: actions/github-script@v6
env:
RUN_ID: ${{ github.event.workflow_run.id }}
with:
@@ -53,7 +53,7 @@ jobs:
run: unzip "${ARTIFACT_FOLDER}/${ARTIFACT_NAME}.zip" -d "${ARTIFACT_FOLDER}"
- name: Parsing levitate result
uses: actions/github-script@v5
uses: actions/github-script@v6
id: levitate-run
with:
script: |
@@ -63,7 +63,7 @@ jobs:
- name: Check if "breaking change" label exists
id: does-label-exist
uses: actions/github-script@v5
uses: actions/github-script@v6
env:
PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }}
with:
@@ -91,7 +91,7 @@ jobs:
${{ steps.levitate-run.outputs.message }}
[Console output](${{ steps.levitate-run.outputs.job_link }})
[Read our guideline](../../contribute/breaking-changes-guide.md)
[Read our guideline](https://github.com/grafana/grafana/blob/main/contribute/breaking-changes-guide.md)
- name: Remove comment on PR
if: ${{ steps.levitate-run.outputs.exit_code == 0 }}
@@ -119,7 +119,7 @@ jobs:
- name: Add "breaking change" label
if: ${{ steps.levitate-run.outputs.exit_code == 1 && steps.does-label-exist.outputs.result == 0 }}
uses: actions/github-script@v5
uses: actions/github-script@v6
env:
PR_NUMBER: ${{ steps.levitate-run.outputs.pr_number }}
with:
@@ -134,7 +134,7 @@ jobs:
- name: Remove "breaking change" label
if: ${{ steps.levitate-run.outputs.exit_code == 0 && steps.does-label-exist.outputs.result == 1 }}
uses: actions/github-script@v5
uses: actions/github-script@v6
env:
PR_NUMBER: ${{ steps.levitate-run.outputs.pr_number }}
with:
@@ -151,7 +151,7 @@ jobs:
# Related issue: https://github.com/renovatebot/renovate/issues/1908
- name: Add "grafana/plugins-platform-frontend" as a reviewer
if: ${{ steps.levitate-run.outputs.exit_code == 1 }}
uses: actions/github-script@v5
uses: actions/github-script@v6
env:
PR_NUMBER: ${{ steps.levitate-run.outputs.pr_number }}
with:
@@ -167,7 +167,7 @@ jobs:
- name: Remove "grafana/plugins-platform-frontend" from the list of reviewers
if: ${{ steps.levitate-run.outputs.exit_code == 0 }}
uses: actions/github-script@v5
uses: actions/github-script@v6
env:
PR_NUMBER: ${{ steps.levitate-run.outputs.pr_number }}
with:
+2 -1
View File
@@ -15,7 +15,8 @@
"name": "Debug Jest test",
"type": "node",
"request": "launch",
"runtimeArgs": ["--inspect-brk", "${workspaceRoot}/node_modules/.bin/jest", "--runInBand", "${file}"],
"runtimeExecutable": "yarn",
"runtimeArgs": ["run", "jest", "--runInBand", "${file}"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"port": 9229
+70
View File
@@ -1,3 +1,73 @@
<!-- 8.4.1 START -->
# 8.4.1 (2022-02-18)
### Features and enhancements
- **Cloudwatch:** Add support for AWS/PrivateLink\* metrics and dimensions. [#45515](https://github.com/grafana/grafana/pull/45515), [@szymonpk](https://github.com/szymonpk)
- **Configuration:** Add ability to customize okta login button name and icon. [#44079](https://github.com/grafana/grafana/pull/44079), [@DanCech](https://github.com/DanCech)
- **Tempo:** Switch out Select with AsyncSelect component to get loading state in Tempo Search. [#45110](https://github.com/grafana/grafana/pull/45110), [@CatPerry](https://github.com/CatPerry)
### Bug fixes
- **Alerting:** Fix migrations by making send_alerts_to field nullable. [#45572](https://github.com/grafana/grafana/pull/45572), [@santihernandezc](https://github.com/santihernandezc)
<!-- 8.4.1 END -->
<!-- 8.4.0 START -->
# 8.4.0 (2022-02-16)
### Features and enhancements
- **API:** Extract OpenAPI specification from source code using go-swagger. [#40528](https://github.com/grafana/grafana/pull/40528), [@papagian](https://github.com/papagian)
- **AccessControl:** Disable user remove and user update roles when they do not have the permissions. [#43429](https://github.com/grafana/grafana/pull/43429), [@Jguer](https://github.com/Jguer)
- **AccessControl:** Provisioning for teams. [#43767](https://github.com/grafana/grafana/pull/43767), [@gamab](https://github.com/gamab)
- **API:** Add usage stats preview endpoint. [#43899](https://github.com/grafana/grafana/pull/43899), [@Jguer](https://github.com/Jguer)
- **Alerting:** Move slow queries in the scheduler to another goroutine. [#44423](https://github.com/grafana/grafana/pull/44423), [@grobinson-grafana](https://github.com/grobinson-grafana)
- **Alerting:** Use time.Ticker instead of alerting.Ticker in ngalert. [#44395](https://github.com/grafana/grafana/pull/44395), [@grobinson-grafana](https://github.com/grobinson-grafana)
- **Alerting:** add custom grouping to Alert Panel. [#44559](https://github.com/grafana/grafana/pull/44559), [@gillesdemey](https://github.com/gillesdemey)
- **Analytics:** Add user id tracking to google analytics. [#42763](https://github.com/grafana/grafana/pull/42763), [@autoric](https://github.com/autoric)
- **Angular:** Add AngularJS plugin support deprecation plan to docs site. [#45149](https://github.com/grafana/grafana/pull/45149), [@torkelo](https://github.com/torkelo)
- **Auth:** implement auto_sign_up for auth.jwt. [#43502](https://github.com/grafana/grafana/pull/43502), [@sakjur](https://github.com/sakjur)
- **Azure Monitor Logs:** Order subscriptions in resource picker by name. [#45228](https://github.com/grafana/grafana/pull/45228), [@sunker](https://github.com/sunker)
- **Azure monitor Logs:** Optimize data fetching in resource picker. [#44549](https://github.com/grafana/grafana/pull/44549), [@sunker](https://github.com/sunker)
- **AzureMonitor:** Filter list of resources by resourceType. [#43522](https://github.com/grafana/grafana/pull/43522), [@andresmgot](https://github.com/andresmgot)
- **BarChart:** color by field, x time field, bar radius, label skipping. [#43257](https://github.com/grafana/grafana/pull/43257), [@leeoniya](https://github.com/leeoniya)
- **Chore:** Implement OpenTelemetry in Grafana. [#42674](https://github.com/grafana/grafana/pull/42674), [@idafurjes](https://github.com/idafurjes)
- **Cloud Monitoring:** Adds metric type to Metric drop down options. [#43268](https://github.com/grafana/grafana/pull/43268), [@tw1nk](https://github.com/tw1nk)
- **CloudWatch:** Add Data Lifecycle Manager metrics and dimension. [#43310](https://github.com/grafana/grafana/pull/43310), [@ilyastoli](https://github.com/ilyastoli)
- **CloudWatch:** Add Missing Elasticache Host-level metrics. [#43455](https://github.com/grafana/grafana/pull/43455), [@dhendo](https://github.com/dhendo)
- **CloudWatch:** Add all ElastiCache Redis Metrics. [#43336](https://github.com/grafana/grafana/pull/43336), [@siavashs](https://github.com/siavashs)
- **CloudWatch:** Add new AWS/ES metrics. [#43034](https://github.com/grafana/grafana/pull/43034), [@sunker](https://github.com/sunker)
- **Cloudwatch:** Add syntax highlighting and autocomplete for "Metric Search". [#43985](https://github.com/grafana/grafana/pull/43985), [@sarahzinger](https://github.com/sarahzinger)
- **Explore:** Support custom display label for exemplar links for Prometheus datasource. [#42732](https://github.com/grafana/grafana/pull/42732), [@JokerQueue](https://github.com/JokerQueue)
- **Hotkeys:** Make time range absolute/permanent. [#43802](https://github.com/grafana/grafana/pull/43802), [@davkal](https://github.com/davkal)
- **Playlists:** Enable sharing direct links to playlists. [#44161](https://github.com/grafana/grafana/pull/44161), [@ashharrison90](https://github.com/ashharrison90)
- **SQLStore:** Prevent concurrent migrations. [#44101](https://github.com/grafana/grafana/pull/44101), [@papagian](https://github.com/papagian)
- **SSE:** Add Mode to drop NaN/Inf/Null in Reduction operations. [#43583](https://github.com/grafana/grafana/pull/43583), [@kylebrandt](https://github.com/kylebrandt)
- **Setting:** Support configuring feature toggles with bools instead of just passing an array. [#43326](https://github.com/grafana/grafana/pull/43326), [@bergquist](https://github.com/bergquist)
- **TimeSeries:** Add support for negative Y and constant transform. [#44774](https://github.com/grafana/grafana/pull/44774), [@dprokop](https://github.com/dprokop)
- **Transformations:** Add 'JSON' field type to ConvertFieldTypeTransformer. [#42624](https://github.com/grafana/grafana/pull/42624), [@sd2k](https://github.com/sd2k)
### Bug fixes
- **Auth:** Guarantee consistency of signed SigV4 headers. [#45054](https://github.com/grafana/grafana/pull/45054), [@wbrowne](https://github.com/wbrowne)
- **CloudWatch:** Fix MetricName resetting on Namespace change. [#44165](https://github.com/grafana/grafana/pull/44165), [@yaelleC](https://github.com/yaelleC)
- **Cloudwatch :** Fixed resetting metric name when changing namespace in Metric Query. [#44612](https://github.com/grafana/grafana/pull/44612), [@yaelleC](https://github.com/yaelleC)
- **Explore:** Avoid locking timepicker when range is inverted. [#44790](https://github.com/grafana/grafana/pull/44790), [@Elfo404](https://github.com/Elfo404)
- **Instrumentation:** Fix HTTP request instrumentation of authentication failures. [#44234](https://github.com/grafana/grafana/pull/44234), [@marefr](https://github.com/marefr)
- **LibraryPanels:** Prevent long descriptions and names from obscuring the delete button. [#45190](https://github.com/grafana/grafana/pull/45190), [@zuchka](https://github.com/zuchka)
- **OAuth:** Fix parsing of ID token if header contains non-string value. [#44159](https://github.com/grafana/grafana/pull/44159), [@marefr](https://github.com/marefr)
- **Panel Edit:** Visualization search now works correctly with special characters. [#45137](https://github.com/grafana/grafana/pull/45137), [@ashharrison90](https://github.com/ashharrison90)
- **Provisioning:** Fix duplicate validation when multiple organizations have been configured. [#44151](https://github.com/grafana/grafana/pull/44151), [@marefr](https://github.com/marefr)
- **QueryField:** Fix issue with undo history when suggestion is inserted (#28656). [#39114](https://github.com/grafana/grafana/pull/39114), [@glintik](https://github.com/glintik)
- **TablePanel:** Do not prefix columns with frame name if multiple frames and override active. [#45174](https://github.com/grafana/grafana/pull/45174), [@mdvictor](https://github.com/mdvictor)
### Deprecations
AngularJS plugin support is now in a deprecated state, meaning it will be removed in a future release. Currently, that is planned for version 10 (in 2023). The documentation site has an [article](https://grafana.com/docs/grafana/next/developers/angular_deprecation/) with more details on why, when, and how. Issue [#45149](https://github.com/grafana/grafana/issues/45149)
<!-- 8.4.0 END -->
<!-- 8.4.0-beta1 START -->
# 8.4.0-beta1 (2022-02-02)
+5
View File
@@ -292,6 +292,9 @@ content_security_policy = false
# $ROOT_PATH is server.root_url without the protocol.
content_security_policy_template = """script-src 'self' 'unsafe-eval' 'unsafe-inline' 'strict-dynamic' $NONCE;object-src 'none';font-src 'self';style-src 'self' 'unsafe-inline' blob:;img-src * data:;base-uri 'self';connect-src 'self' grafana.com ws://$ROOT_PATH wss://$ROOT_PATH;manifest-src 'self';media-src 'none';form-action 'self';"""
# Controls if old angular plugins are supported or not. This will be disabled by default in Grafana v9.
angular_support_enabled = true
#################################### Snapshots ###########################
[snapshots]
# snapshot sharing options
@@ -497,6 +500,7 @@ allowed_groups =
#################################### Okta OAuth #######################
[auth.okta]
name = Okta
icon = okta
enabled = false
allow_sign_up = true
client_id = some_id
@@ -513,6 +517,7 @@ role_attribute_strict = false
#################################### Generic OAuth #######################
[auth.generic_oauth]
name = OAuth
icon = signin
enabled = false
allow_sign_up = true
client_id = some_id
+3
View File
@@ -292,6 +292,9 @@
# $ROOT_PATH is server.root_url without the protocol.
;content_security_policy_template = """script-src 'self' 'unsafe-eval' 'unsafe-inline' 'strict-dynamic' $NONCE;object-src 'none';font-src 'self';style-src 'self' 'unsafe-inline' blob:;img-src * data:;base-uri 'self';connect-src 'self' grafana.com ws://$ROOT_PATH wss://$ROOT_PATH;manifest-src 'self';media-src 'none';form-action 'self';"""
# Controls if old angular plugins are supported or not. This will be disabled by default in Grafana v9.
;angular_support_enabled = true
#################################### Snapshots ###########################
[snapshots]
# snapshot sharing options
-1
View File
@@ -1 +0,0 @@
/Users/mitchel/Workspace/grafana-enterprise/src/devenv/blocks/saml-enterprise
+1 -1
View File
@@ -10,7 +10,7 @@ Yarn >= 1.22.4
## Build the doc site
1. On the command line, first change to the docs folder: `cd docs`.
1. Run `make docs-quick`. This launches a preview of the website with the current grafana docs at `http://localhost:3002/docs/grafana/next/` which will refresh automatically when changes are made to content in the `sources` directory.
1. Run `make docs`. This launches a preview of the website with the current grafana docs at `http://localhost:3002/docs/grafana/next/` which will refresh automatically when changes are made to content in the `sources` directory.
If you have the grafana/website repo checked out in the same directory as the grafana repo, then you can run `make docs-local-static` to use local assets (such as images).
@@ -582,6 +582,21 @@ Set Content Security Policy template used when adding the Content-Security-Polic
<hr />
### angular_support_enabled
This currently defaults to `true` but will in Grafana v9 default to `false`. When set to false the angular framework and support components will not be loaded. This means that
all plugins and core features that depend on angular support will stop working.
Current core features that will stop working:
- Heatmap panel
- Old graph panel
- Old table panel
- Postgres, MySQL and MSSQL data source query editors
- Legacy alerting edit rule UI
Before we disable angular support by default we plan to migrate these remaining areas to React.
## [snapshots]
### external_enabled
+2
View File
@@ -27,6 +27,8 @@ Example config:
```bash
[auth.generic_oauth]
name = OAuth
icon = signin
enabled = true
client_id = YOUR_APP_CLIENT_ID
client_secret = YOUR_APP_CLIENT_SECRET
+1
View File
@@ -38,6 +38,7 @@ Before you can sign a user in, you need to create an Okta application from the O
```ini
[auth.okta]
name = Okta
icon = okta
enabled = true
allow_sign_up = true
client_id = some_id
+15 -2
View File
@@ -55,11 +55,11 @@ By default, data source queries are not cached. To enable query caching for a si
1. In the data source list, click the data source that you want to turn on caching for.
1. Open the Cache tab.
1. Press the Enable button.
1. (Optional) Choose a custom TTL for that data source. If you skip this step, then Grafana uses the default TTL.
1. (Optional) Choose custom TTLs for the data source's queries and resources caching. If you skip this step, then Grafana uses the default TTL.
> **Note:** If query caching is enabled and the Cache tab is not visible in a data source's settings, then query caching is not available for that data source.
To configure global settings for query caching, refer the the [Query caching section of Enterprise Configuration]({{< relref "./enterprise-configuration.md#caching" >}}).
To configure global settings for query caching, refer to the [Query caching section of Enterprise Configuration]({{< relref "./enterprise-configuration.md#caching" >}}).
## Disable query caching
@@ -71,6 +71,19 @@ To disable query caching for a single data source:
To disable query caching for an entire Grafana instance, set the `enabled` flag to `false` in the [Query caching section of Enterprise Configuration]({{< relref "./enterprise-configuration.md#caching" >}}). You will no longer see the Cache tab on any data sources, and no data source queries will be cached.
## Clear cache
If you experience performance issues or repeated queries become slower to execute, consider clearing your cache.
> **Note:** This action impacts all cache-enabled data sources. If you are using Memcached, the system clears all data from the Memcached instance.
**To clear the cache**:
1. Sign in to Grafana and click **Settings > Data Sources**.
1. Select a data source.
1. Click the **Cache** tab.
1. Click **Clear cache**.
## Sending a request without cache
If a data source query request contains an `X-Cache-Skip` header, then Grafana skips the caching middleware, and does not search the cache for a response. This can be particularly useful when debugging data source queries using cURL.
+3 -1
View File
@@ -29,7 +29,9 @@ If the data source does not support loading full range log volume histogram, the
#### Log level
For logs where a level label is specified, we use the value of the label to determine the log level and update color accordingly. If the log doesn't have a level label specified, we try to parse the log using logfmt and JSON parsers to find out if its content matches any of the supported expressions (see below for more information). The log level is always determined by the first match. In case Grafana is not able to determine a log level, it will be visualized with an unknown log level.
For logs where a level label is specified, we use the value of the label to determine the log level and update color accordingly. If the log doesn't have a level label specified, we try to find out if its content matches any of the supported expressions (see below for more information). The log level is always determined by the first match. In case Grafana is not able to determine a log level, it will be visualized with an unknown log level.
> **Tip:** If you use Loki data source and the "level" is in you log content, try to use parsers (JSON, logfmt, regex,..) to extract level information into level label that is used to determine log level.
**Supported log levels and mapping of log level abbreviation and expressions:**
@@ -16,8 +16,6 @@ Expressions are primarily used by the new [Grafana 8 alerts]({{< relref "../../.
> **Note:** Expressions do not work with legacy dashboard alerts.
> **Note:** Expressions do not work with dashboard variables.
Expressions are meant to augment data sources by enabling queries from different data sources to be combined or by providing operations unavailable in a data source.
> **Note:** When possible, you should do data processing inside the data source. Copying data from storage to the Grafana server for processing is inefficient, so expressions are targeted at lightweight data processing.
+1
View File
@@ -8,6 +8,7 @@ weight = 10000
Here you can find detailed release notes that list everything that is included in every release as well as notices
about deprecations, breaking changes as well as changes that relate to plugin development.
- [Release notes for 8.4.1]({{< relref "release-notes-8-4-1" >}})
- [Release notes for 8.4.0-beta1]({{< relref "release-notes-8-4-0-beta1" >}})
- [Release notes for 8.3.6]({{< relref "release-notes-8-3-6" >}})
- [Release notes for 8.3.5]({{< relref "release-notes-8-3-5" >}})
@@ -0,0 +1,58 @@
+++
title = "Release notes for Grafana 8.4.0"
hide_menu = true
+++
<!-- Auto generated by update changelog github action -->
# Release notes for Grafana 8.4.0
### Features and enhancements
- **API:** Extract OpenAPI specification from source code using go-swagger. [#40528](https://github.com/grafana/grafana/pull/40528), [@papagian](https://github.com/papagian)
- **AccessControl:** Disable user remove and user update roles when they do not have the permissions. [#43429](https://github.com/grafana/grafana/pull/43429), [@Jguer](https://github.com/Jguer)
- **AccessControl:** Provisioning for teams. [#43767](https://github.com/grafana/grafana/pull/43767), [@gamab](https://github.com/gamab)
- **API:** Add usage stats preview endpoint. [#43899](https://github.com/grafana/grafana/pull/43899), [@Jguer](https://github.com/Jguer)
- **Alerting:** Move slow queries in the scheduler to another goroutine. [#44423](https://github.com/grafana/grafana/pull/44423), [@grobinson-grafana](https://github.com/grobinson-grafana)
- **Alerting:** Use time.Ticker instead of alerting.Ticker in ngalert. [#44395](https://github.com/grafana/grafana/pull/44395), [@grobinson-grafana](https://github.com/grobinson-grafana)
- **Alerting:** add custom grouping to Alert Panel. [#44559](https://github.com/grafana/grafana/pull/44559), [@gillesdemey](https://github.com/gillesdemey)
- **Analytics:** Add user id tracking to google analytics. [#42763](https://github.com/grafana/grafana/pull/42763), [@autoric](https://github.com/autoric)
- **Angular:** Add AngularJS plugin support deprecation plan to docs site. [#45149](https://github.com/grafana/grafana/pull/45149), [@torkelo](https://github.com/torkelo)
- **Auth:** implement auto_sign_up for auth.jwt. [#43502](https://github.com/grafana/grafana/pull/43502), [@sakjur](https://github.com/sakjur)
- **Azure Monitor Logs:** Order subscriptions in resource picker by name. [#45228](https://github.com/grafana/grafana/pull/45228), [@sunker](https://github.com/sunker)
- **Azure monitor Logs:** Optimize data fetching in resource picker. [#44549](https://github.com/grafana/grafana/pull/44549), [@sunker](https://github.com/sunker)
- **AzureMonitor:** Filter list of resources by resourceType. [#43522](https://github.com/grafana/grafana/pull/43522), [@andresmgot](https://github.com/andresmgot)
- **BarChart:** color by field, x time field, bar radius, label skipping. [#43257](https://github.com/grafana/grafana/pull/43257), [@leeoniya](https://github.com/leeoniya)
- **Chore:** Implement OpenTelemetry in Grafana. [#42674](https://github.com/grafana/grafana/pull/42674), [@idafurjes](https://github.com/idafurjes)
- **Cloud Monitoring:** Adds metric type to Metric drop down options. [#43268](https://github.com/grafana/grafana/pull/43268), [@tw1nk](https://github.com/tw1nk)
- **CloudWatch:** Add Data Lifecycle Manager metrics and dimension. [#43310](https://github.com/grafana/grafana/pull/43310), [@ilyastoli](https://github.com/ilyastoli)
- **CloudWatch:** Add Missing Elasticache Host-level metrics. [#43455](https://github.com/grafana/grafana/pull/43455), [@dhendo](https://github.com/dhendo)
- **CloudWatch:** Add all ElastiCache Redis Metrics. [#43336](https://github.com/grafana/grafana/pull/43336), [@siavashs](https://github.com/siavashs)
- **CloudWatch:** Add new AWS/ES metrics. [#43034](https://github.com/grafana/grafana/pull/43034), [@sunker](https://github.com/sunker)
- **Cloudwatch:** Add syntax highlighting and autocomplete for "Metric Search". [#43985](https://github.com/grafana/grafana/pull/43985), [@sarahzinger](https://github.com/sarahzinger)
- **Explore:** Support custom display label for exemplar links for Prometheus datasource. [#42732](https://github.com/grafana/grafana/pull/42732), [@JokerQueue](https://github.com/JokerQueue)
- **Hotkeys:** Make time range absolute/permanent. [#43802](https://github.com/grafana/grafana/pull/43802), [@davkal](https://github.com/davkal)
- **Playlists:** Enable sharing direct links to playlists. [#44161](https://github.com/grafana/grafana/pull/44161), [@ashharrison90](https://github.com/ashharrison90)
- **SQLStore:** Prevent concurrent migrations. [#44101](https://github.com/grafana/grafana/pull/44101), [@papagian](https://github.com/papagian)
- **SSE:** Add Mode to drop NaN/Inf/Null in Reduction operations. [#43583](https://github.com/grafana/grafana/pull/43583), [@kylebrandt](https://github.com/kylebrandt)
- **Setting:** Support configuring feature toggles with bools instead of just passing an array. [#43326](https://github.com/grafana/grafana/pull/43326), [@bergquist](https://github.com/bergquist)
- **TimeSeries:** Add support for negative Y and constant transform. [#44774](https://github.com/grafana/grafana/pull/44774), [@dprokop](https://github.com/dprokop)
- **Transformations:** Add 'JSON' field type to ConvertFieldTypeTransformer. [#42624](https://github.com/grafana/grafana/pull/42624), [@sd2k](https://github.com/sd2k)
### Bug fixes
- **Auth:** Guarantee consistency of signed SigV4 headers. [#45054](https://github.com/grafana/grafana/pull/45054), [@wbrowne](https://github.com/wbrowne)
- **CloudWatch:** Fix MetricName resetting on Namespace change. [#44165](https://github.com/grafana/grafana/pull/44165), [@yaelleC](https://github.com/yaelleC)
- **Cloudwatch :** Fixed resetting metric name when changing namespace in Metric Query. [#44612](https://github.com/grafana/grafana/pull/44612), [@yaelleC](https://github.com/yaelleC)
- **Explore:** Avoid locking timepicker when range is inverted. [#44790](https://github.com/grafana/grafana/pull/44790), [@Elfo404](https://github.com/Elfo404)
- **Instrumentation:** Fix HTTP request instrumentation of authentication failures. [#44234](https://github.com/grafana/grafana/pull/44234), [@marefr](https://github.com/marefr)
- **LibraryPanels:** Prevent long descriptions and names from obscuring the delete button. [#45190](https://github.com/grafana/grafana/pull/45190), [@zuchka](https://github.com/zuchka)
- **OAuth:** Fix parsing of ID token if header contains non-string value. [#44159](https://github.com/grafana/grafana/pull/44159), [@marefr](https://github.com/marefr)
- **Panel Edit:** Visualization search now works correctly with special characters. [#45137](https://github.com/grafana/grafana/pull/45137), [@ashharrison90](https://github.com/ashharrison90)
- **Provisioning:** Fix duplicate validation when multiple organizations have been configured. [#44151](https://github.com/grafana/grafana/pull/44151), [@marefr](https://github.com/marefr)
- **QueryField:** Fix issue with undo history when suggestion is inserted (#28656). [#39114](https://github.com/grafana/grafana/pull/39114), [@glintik](https://github.com/glintik)
- **TablePanel:** Do not prefix columns with frame name if multiple frames and override active. [#45174](https://github.com/grafana/grafana/pull/45174), [@mdvictor](https://github.com/mdvictor)
### Deprecations
AngularJS plugin support is now in a deprecated state, meaning it will be removed in a future release. Currently, that is planned for version 10 (in 2023). The documentation site has an [article](https://grafana.com/docs/grafana/next/developers/angular_deprecation/) with more details on why, when, and how. Issue [#45149](https://github.com/grafana/grafana/issues/45149)
@@ -0,0 +1,18 @@
+++
title = "Release notes for Grafana 8.4.1"
hide_menu = true
+++
<!-- Auto generated by update changelog github action -->
# Release notes for Grafana 8.4.1
### Features and enhancements
- **Cloudwatch:** Add support for AWS/PrivateLink\* metrics and dimensions. [#45515](https://github.com/grafana/grafana/pull/45515), [@szymonpk](https://github.com/szymonpk)
- **Configuration:** Add ability to customize okta login button name and icon. [#44079](https://github.com/grafana/grafana/pull/44079), [@DanCech](https://github.com/DanCech)
- **Tempo:** Switch out Select with AsyncSelect component to get loading state in Tempo Search. [#45110](https://github.com/grafana/grafana/pull/45110), [@CatPerry](https://github.com/CatPerry)
### Bug fixes
- **Alerting:** Fix migrations by making send_alerts_to field nullable. [#45572](https://github.com/grafana/grafana/pull/45572), [@santihernandezc](https://github.com/santihernandezc)
+1 -1
View File
@@ -17,7 +17,7 @@ To limit the number of lines rendered, you can use the **Max data points** setti
## Log level
For logs where a **level** label is specified, we use the value of the label to determine the log level and update color accordingly. If the log doesn't have a level label specified, we parse the log to find out if its content matches any of the supported expressions (see below for more information). The log level is always determined by the first match. In case Grafana is not able to determine a log level, it will be visualized with **unknown** log level. See [supported log levels and mappings of log level abbreviation and expressions]({{< relref "../explore/_index.md#log-level" >}}).
For logs where a **level** label is specified, we use the value of the label to determine the log level and update color accordingly. If the log doesn't have a level label specified, we try to find out if its content matches any of the supported expressions (see below for more information). The log level is always determined by the first match. In case Grafana is not able to determine a log level, it will be visualized with **unknown** log level. See [supported log levels and mappings of log level abbreviation and expressions]({{< relref "../explore/_index.md#log-level" >}}).
## Log details
+2 -2
View File
@@ -24,9 +24,9 @@ You can now share links to your playlists the same way as with dashboards, to ea
{{< figure src="/static/img/docs/dashboards/share-playlists-8-4.png" max-width="1200px" caption="Share playlist" >}}
### $**interval and $**interval_ms in panel title
### `$__interval` and `$__interval_ms` in panel title
We’ve expanded the functionality of this existing and popular feature. You can now use $**interval and $**interval_ms in panel titles. This new function displays the interval that’s used in a specific panel without requiring edit mode.
We’ve expanded the functionality of this existing and popular feature. You can now use `$__interval` and `$__interval_ms` in panel titles. This new function displays the interval that’s used in a specific panel without requiring edit mode.
{{< figure src="/static/img/docs/panels/interval-8-4.png" max-width="1200px" caption="Time intervals" >}}
+22
View File
@@ -19,6 +19,28 @@ e2e.scenario({
cy.contains('CSV Metric Values').scrollIntoView().should('be.visible').click();
cy.location().then((loc) => {
const params = new URLSearchParams(loc.search);
const leftJSON = JSON.parse(params.get('left'));
expect(leftJSON.range.to).to.equal('now');
expect(leftJSON.range.from).to.equal('now-1h');
cy.get('body').click();
cy.get('body').type('t{leftarrow}');
cy.location().then((locPostKeypress) => {
const params = new URLSearchParams(locPostKeypress.search);
const leftJSON = JSON.parse(params.get('left'));
// be sure the keypress affected the time window
expect(leftJSON.range.to).to.not.equal('now');
expect(leftJSON.range.from).to.not.equal('now-1h');
// be sure the url does not contain dashboard range values
// eslint wants this to be a function, so we use this instead of to.be.false
expect(params.has('to')).to.equal(false);
expect(params.has('from')).to.equal(false);
});
});
const canvases = e2e().get('canvas');
canvases.should('have.length', 1);
},
+1 -1
View File
@@ -13,7 +13,7 @@ module.exports = {
'node_modules/(?!(ol)/)', // <- exclude the open layers library
],
moduleDirectories: ['node_modules', 'public'],
roots: ['<rootDir>/public/app', '<rootDir>/public/test', '<rootDir>/packages', '<rootDir>/scripts'],
roots: ['<rootDir>/public/app', '<rootDir>/public/test', '<rootDir>/packages'],
testRegex: '(\\.|/)(test)\\.(jsx?|tsx?)$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
setupFiles: ['jest-canvas-mock', './public/test/jest-shim.ts', './public/test/jest-setup.ts'],
+2 -2
View File
@@ -1,4 +1,4 @@
{
"stable": "8.3.6",
"testing": "8.4.0-beta1"
"stable": "8.4.1",
"testing": "8.4.1"
}
View File
+13 -14
View File
@@ -71,7 +71,7 @@
"devDependencies": {
"@babel/core": "7.17.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.16.7",
"@babel/plugin-proposal-object-rest-spread": "7.16.7",
"@babel/plugin-proposal-object-rest-spread": "7.17.3",
"@babel/plugin-proposal-optional-chaining": "7.16.7",
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/plugin-transform-react-constant-elements": "7.16.7",
@@ -93,9 +93,9 @@
"@microsoft/api-extractor": "7.19.4",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.4",
"@react-types/button": "3.4.1",
"@react-types/menu": "3.4.1",
"@react-types/menu": "3.5.1",
"@react-types/overlays": "3.5.1",
"@react-types/shared": "3.10.1",
"@react-types/shared": "3.11.1",
"@rtsao/plugin-proposal-class-properties": "7.0.1-patch.1",
"@swc/core": "1.2.136",
"@swc/helpers": "0.3.2",
@@ -126,7 +126,6 @@
"@types/lingui__macro": "^3",
"@types/lodash": "4.14.149",
"@types/logfmt": "^1.2.1",
"@types/lru-cache": "^5.1.0",
"@types/mousetrap": "1.6.3",
"@types/node": "16.11.22",
"@types/papaparse": "5.3.2",
@@ -158,7 +157,7 @@
"@typescript-eslint/parser": "5.11.0",
"@wojtekmaj/enzyme-adapter-react-17": "0.6.6",
"autoprefixer": "10.4.2",
"axios": "0.25.0",
"axios": "0.26.0",
"babel-jest": "27.5.0",
"babel-loader": "8.2.3",
"babel-plugin-angularjs-annotate": "0.10.0",
@@ -169,11 +168,11 @@
"cypress": "9.3.1",
"enzyme": "3.11.0",
"enzyme-to-json": "3.6.2",
"eslint": "8.8.0",
"eslint-config-prettier": "8.3.0",
"eslint": "8.9.0",
"eslint-config-prettier": "8.4.0",
"eslint-plugin-jest": "^26.1.0",
"eslint-plugin-jsdoc": "37.8.0",
"eslint-plugin-lodash": "^7.2.0",
"eslint-plugin-jsdoc": "37.9.1",
"eslint-plugin-lodash": "7.4.0",
"eslint-plugin-prettier": "4.0.0",
"eslint-plugin-react": "7.28.0",
"eslint-plugin-react-hooks": "4.3.0",
@@ -213,7 +212,7 @@
"redux-mock-store": "1.5.4",
"rimraf": "3.0.2",
"sass": "1.49.7",
"sass-loader": "12.4.0",
"sass-loader": "12.6.0",
"sinon": "13.0.1",
"style-loader": "3.3.1",
"stylelint": "14.5.0",
@@ -234,7 +233,7 @@
},
"dependencies": {
"@emotion/css": "11.7.1",
"@emotion/react": "11.7.1",
"@emotion/react": "11.8.1",
"@grafana/aws-sdk": "0.0.33",
"@grafana/data": "workspace:*",
"@grafana/e2e-selectors": "workspace:*",
@@ -255,7 +254,7 @@
"@react-aria/button": "3.3.4",
"@react-aria/dialog": "3.1.4",
"@react-aria/focus": "3.5.0",
"@react-aria/interactions": "3.7.0",
"@react-aria/interactions": "3.8.1",
"@react-aria/menu": "3.3.0",
"@react-aria/overlays": "3.7.3",
"@react-aria/utils": "3.11.0",
@@ -309,7 +308,7 @@
"lezer-tree": "0.13.2",
"lodash": "4.17.21",
"logfmt": "^1.3.2",
"lru-cache": "6.0.0",
"lru-cache": "7.3.1",
"memoize-one": "6.0.0",
"moment": "2.29.1",
"moment-timezone": "0.5.34",
@@ -335,7 +334,7 @@
"react-draggable": "4.4.4",
"react-grid-layout": "1.3.3",
"react-highlight-words": "0.17.0",
"react-hook-form": "7.5.3",
"react-hook-form": "7.27.0",
"react-inlinesvg": "2.3.0",
"react-loadable": "5.5.0",
"react-moveable": "0.30.3",
@@ -285,15 +285,24 @@ export function buildHistogram(frames: DataFrame[], options?: HistogramTransform
};
}
// function incrRound(num: number, incr: number) {
// return Math.round(num / incr) * incr;
// }
/**
* @internal
*/
export function incrRound(num: number, incr: number) {
return Math.round(num / incr) * incr;
}
// function incrRoundUp(num: number, incr: number) {
// return Math.ceil(num / incr) * incr;
// }
/**
* @internal
*/
export function incrRoundUp(num: number, incr: number) {
return Math.ceil(num / incr) * incr;
}
function incrRoundDn(num: number, incr: number) {
/**
* @internal
*/
export function incrRoundDn(num: number, incr: number) {
return Math.floor(num / incr) * incr;
}
@@ -28,6 +28,7 @@ export enum DataTransformerID {
prepareTimeSeries = 'prepareTimeSeries',
convertFieldType = 'convertFieldType',
fieldLookup = 'fieldLookup',
heatmap = 'heatmap',
spatial = 'spatial',
extractFields = 'extractFields',
}
+2 -1
View File
@@ -83,7 +83,7 @@ export type OAuth =
*
* @public
*/
export type OAuthSettings = Partial<Record<OAuth, { name: string }>>;
export type OAuthSettings = Partial<Record<OAuth, { name: string; icon?: string }>>;
/**
* Describes all the different Grafana configuration values available for an instance.
@@ -138,4 +138,5 @@ export interface GrafanaConfig {
geomapDefaultBaseLayer?: MapLayerOptions;
geomapDisableCustomBaseLayer?: boolean;
unifiedAlertingEnabled: boolean;
angularSupportEnabled: boolean;
}
@@ -8,4 +8,17 @@ export enum DataFrameType {
TimeSeriesWide = 'timeseries-wide',
TimeSeriesLong = 'timeseries-long',
TimeSeriesMany = 'timeseries-many',
/**
* First field is X, the rest are bucket values
*/
HeatmapBuckets = 'heatmap-buckets',
/**
* Explicit fields for:
* xMin, yMin, count, ...
*
* All values in the grid exist and have regular spacing
*/
HeatmapScanlines = 'heatmap-scanlines',
}
@@ -23,6 +23,7 @@ export interface FeatureToggles {
database_metrics?: boolean;
dashboardPreviews?: boolean;
dashboardPreviewsScheduler?: boolean;
dashboardPreviewsAdmin?: boolean;
['live-config']?: boolean;
['live-pipeline']?: boolean;
['live-service-web-worker']?: boolean;
@@ -20,6 +20,7 @@ export interface NavModelItem {
onClick?: () => void;
menuItemType?: NavMenuItemType;
highlightText?: string;
highlightId?: string;
tabSuffix?: ComponentType<{ className?: string }>;
}
+8
View File
@@ -41,6 +41,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
alertingErrorOrTimeout = '';
alertingNoDataOrNullValues = '';
alertingMinInterval = 1;
angularSupportEnabled = false;
authProxyEnabled = false;
exploreEnabled = false;
ldapEnabled = false;
@@ -65,6 +66,13 @@ export class GrafanaBootConfig implements GrafanaConfig {
featureToggles: FeatureToggles = {};
licenseInfo: LicenseInfo = {} as LicenseInfo;
rendererAvailable = false;
dashboardPreviews: {
systemRequirements: {
met: boolean;
requiredImageRendererPluginVersion: string;
};
thumbnailsExist: boolean;
} = { systemRequirements: { met: false, requiredImageRendererPluginVersion: '' }, thumbnailsExist: false };
rendererVersion = '';
http2Enabled = false;
dateFormats?: SystemDateFormatSettings;
@@ -16,6 +16,7 @@ import (
ptable "github.com/grafana/grafana/public/app/plugins/panel/table:grafanaschema"
ptext "github.com/grafana/grafana/public/app/plugins/panel/text:grafanaschema"
ptimeseries "github.com/grafana/grafana/public/app/plugins/panel/timeseries:grafanaschema"
pheatmap_new "github.com/grafana/grafana/public/app/plugins/panel/heatmap-new:grafanaschema"
)
// Family composes the base dashboard scuemata family with all Grafana core plugins -
@@ -40,5 +41,6 @@ Family: dashboard.Family & {
text: ptext.Panel
table: ptable.Panel
timeseries: ptimeseries.Panel
"heatmap-new": pheatmap_new.Panel
}
}
+3 -3
View File
@@ -31,7 +31,7 @@
],
"dependencies": {
"@emotion/css": "11.7.1",
"@emotion/react": "11.7.1",
"@emotion/react": "11.8.1",
"@grafana/aws-sdk": "0.0.33",
"@grafana/data": "8.5.0-pre",
"@grafana/e2e-selectors": "8.5.0-pre",
@@ -74,7 +74,7 @@
"react-dom": "17.0.2",
"react-dropzone": "11.5.1",
"react-highlight-words": "0.17.0",
"react-hook-form": "7.5.3",
"react-hook-form": "7.27.0",
"react-inlinesvg": "2.3.0",
"react-popper": "2.2.5",
"react-popper-tooltip": "^4.3.1",
@@ -169,7 +169,7 @@
"rollup": "2.67.1",
"rollup-plugin-sourcemaps": "0.6.3",
"rollup-plugin-terser": "7.0.2",
"sass-loader": "12.4.0",
"sass-loader": "12.6.0",
"storybook-dark-mode": "1.0.8",
"style-loader": "3.3.1",
"terser-webpack-plugin": "5.3.1",
@@ -19,6 +19,7 @@ export interface Props extends HTMLAttributes<HTMLDivElement> {
elevated?: boolean;
buttonContent?: React.ReactNode | string;
bottomSpacing?: number;
topSpacing?: number;
}
function getIconFromSeverity(severity: AlertVariant): string {
@@ -37,11 +38,22 @@ function getIconFromSeverity(severity: AlertVariant): string {
export const Alert = React.forwardRef<HTMLDivElement, Props>(
(
{ title, onRemove, children, buttonContent, elevated, bottomSpacing, className, severity = 'error', ...restProps },
{
title,
onRemove,
children,
buttonContent,
elevated,
bottomSpacing,
topSpacing,
className,
severity = 'error',
...restProps
},
ref
) => {
const theme = useTheme2();
const styles = getStyles(theme, severity, elevated, bottomSpacing);
const styles = getStyles(theme, severity, elevated, bottomSpacing, topSpacing);
return (
<div
@@ -77,7 +89,13 @@ export const Alert = React.forwardRef<HTMLDivElement, Props>(
Alert.displayName = 'Alert';
const getStyles = (theme: GrafanaTheme2, severity: AlertVariant, elevated?: boolean, bottomSpacing?: number) => {
const getStyles = (
theme: GrafanaTheme2,
severity: AlertVariant,
elevated?: boolean,
bottomSpacing?: number,
topSpacing?: number
) => {
const color = theme.colors[severity];
const borderRadius = theme.shape.borderRadius();
@@ -92,6 +110,7 @@ const getStyles = (theme: GrafanaTheme2, severity: AlertVariant, elevated?: bool
background: ${theme.colors.background.secondary};
box-shadow: ${elevated ? theme.shadows.z3 : theme.shadows.z1};
margin-bottom: ${theme.spacing(bottomSpacing ?? 2)};
margin-top: ${theme.spacing(topSpacing ?? 0)};
&:before {
content: '';
@@ -1,6 +1,6 @@
import React from 'react';
import { shallow } from 'enzyme';
import { BigValue, Props, BigValueColorMode, BigValueGraphMode } from './BigValue';
import { render, screen } from '@testing-library/react';
import { BigValue, BigValueColorMode, BigValueGraphMode, Props } from './BigValue';
import { createTheme } from '@grafana/data';
function getProps(propOverrides?: Partial<Props>): Props {
@@ -21,22 +21,12 @@ function getProps(propOverrides?: Partial<Props>): Props {
return props;
}
const setup = (propOverrides?: object) => {
const props = getProps(propOverrides);
const wrapper = shallow(<BigValue {...props} />);
const instance = wrapper.instance() as BigValue;
return {
instance,
wrapper,
};
};
describe('BigValue', () => {
describe('Render with basic options', () => {
it('should render', () => {
const { wrapper } = setup();
expect(wrapper).toMatchSnapshot();
render(<BigValue {...getProps()} />);
expect(screen.getByText('25')).toBeInTheDocument();
});
});
});
@@ -1,8 +1,6 @@
// Library
import React, { PureComponent } from 'react';
import { DisplayValue, DisplayValueAlignmentFactors, FieldSparkline, TextDisplayOptions } from '@grafana/data';
// Types
import { Themeable2 } from '../../types';
import { buildLayout } from './BigValueLayout';
import { FormattedValueDisplay } from '../FormattedValueDisplay/FormattedValueDisplay';
@@ -1,54 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BigValue Render with basic options should render 1`] = `
<div
style={
Object {
"alignItems": "center",
"background": "linear-gradient(120deg, rgb(179, 24, 0), rgb(230, 0, 31))",
"borderRadius": "3px",
"display": "flex",
"flexDirection": "row",
"height": "300px",
"padding": "12px",
"position": "relative",
"width": "300px",
}
}
>
<div
style={
Object {
"alignItems": "center",
"display": "flex",
"flexDirection": "column",
"flexGrow": 1,
"justifyContent": "center",
}
}
>
<FormattedDisplayValue
style={
Object {
"color": "rgb(247, 248, 250)",
"fontSize": 230,
"fontWeight": 500,
"lineHeight": 1.2,
"position": "relative",
"textAlign": "center",
"zIndex": 1,
}
}
value={
Object {
"color": "red",
"numeric": 25,
"text": "25",
"titleToAlignTo": undefined,
"valueToAlignTo": "25",
}
}
/>
</div>
</div>
`;
@@ -1,33 +1,39 @@
import React from 'react';
import { render } from 'enzyme';
import { render, screen } from '@testing-library/react';
import { CallToActionCard } from './CallToActionCard';
import { expect } from '../../../../../public/test/lib/common';
describe('CallToActionCard', () => {
describe('rendering', () => {
it('when no message and footer provided', () => {
const tree = render(<CallToActionCard callToActionElement={<a href="http://dummy.link">Click me</a>} />);
expect(tree).toMatchSnapshot();
it('should render callToActionElement', () => {
render(<CallToActionCard callToActionElement={<a href="http://dummy.link">Click me</a>} />);
expect(screen.getByRole('link', { name: 'Click me' })).toBeInTheDocument();
});
it('when message and no footer provided', () => {
const tree = render(
<CallToActionCard
message="Click button bellow"
callToActionElement={<a href="http://dummy.link">Click me</a>}
/>
it('should render message when provided', () => {
render(
<CallToActionCard message="Click button below" callToActionElement={<a href="http://dummy.link">Click me</a>} />
);
expect(tree).toMatchSnapshot();
expect(screen.getByText('Click button below')).toBeInTheDocument();
});
it('when message and footer provided', () => {
const tree = render(
it('should render footer when provided', () => {
render(
<CallToActionCard footer="footer content" callToActionElement={<a href="http://dummy.link">Click me</a>} />
);
expect(screen.getByText('footer content')).toBeInTheDocument();
});
it('should render both message and footer when provided', () => {
render(
<CallToActionCard
message="Click button bellow"
message="Click button below"
footer="footer content"
callToActionElement={<a href="http://dummy.link">Click me</a>}
/>
);
expect(tree).toMatchSnapshot();
expect(screen.getByText('Click button below')).toBeInTheDocument();
expect(screen.getByText('footer content')).toBeInTheDocument();
});
});
});
@@ -1,52 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CallToActionCard rendering when message and footer provided 1`] = `
<div
class="css-xtd6ws-call-to-action-card"
>
<div
class="css-m2iibx"
>
Click button bellow
</div>
<a
href="http://dummy.link"
>
Click me
</a>
<div
class="css-11lqfaj"
>
footer content
</div>
</div>
`;
exports[`CallToActionCard rendering when message and no footer provided 1`] = `
<div
class="css-xtd6ws-call-to-action-card"
>
<div
class="css-m2iibx"
>
Click button bellow
</div>
<a
href="http://dummy.link"
>
Click me
</a>
</div>
`;
exports[`CallToActionCard rendering when no message and footer provided 1`] = `
<div
class="css-xtd6ws-call-to-action-card"
>
<a
href="http://dummy.link"
>
Click me
</a>
</div>
`;
@@ -1,34 +1,72 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ConfirmButton } from './ConfirmButton';
import { mount, ShallowWrapper } from 'enzyme';
import { Button } from '../Button';
import { expect } from '../../../../../public/test/lib/common';
describe('ConfirmButton', () => {
let wrapper: any;
let deleted: any;
beforeAll(() => {
deleted = false;
function deleteItem() {
deleted = true;
}
wrapper = mount(
<ConfirmButton confirmText="Confirm delete" onConfirm={() => deleteItem()}>
it('should show confirm delete when clicked', () => {
const onConfirm = jest.fn();
render(
<ConfirmButton confirmText="Confirm delete" onConfirm={onConfirm}>
Delete
</ConfirmButton>
);
// Confirm button should not be visible before clicking the Delete button
expect(screen.queryByRole('button', { name: 'Confirm delete' })).not.toBeInTheDocument();
userEvent.click(screen.getByRole('button', { name: 'Delete' }));
// Confirm button should now be visible
expect(screen.getByRole('button', { name: 'Confirm delete' })).toBeInTheDocument();
userEvent.click(screen.getByRole('button', { name: 'Confirm delete' }));
expect(onConfirm).toHaveBeenCalled();
// Confirm button should be visible if closeOnConfirm is false
expect(screen.queryByRole('button', { name: 'Confirm delete' })).toBeInTheDocument();
});
it('should show confirm delete when clicked', () => {
expect(deleted).toBe(false);
wrapper
.find(Button)
.findWhere((n: ShallowWrapper) => {
return n.text() === 'Confirm delete' && n.type() === Button;
})
.simulate('click');
expect(deleted).toBe(true);
it('should hide confirm delete when closeOnConfirm is true', () => {
render(
<ConfirmButton confirmText="Confirm delete" onConfirm={() => {}} closeOnConfirm={true}>
Delete
</ConfirmButton>
);
// Confirm button should not be visible before clicking the Delete button
expect(screen.queryByRole('button', { name: 'Confirm delete' })).not.toBeInTheDocument();
userEvent.click(screen.getByRole('button', { name: 'Delete' }));
// Confirm button should now be visible
expect(screen.getByRole('button', { name: 'Confirm delete' })).toBeInTheDocument();
userEvent.click(screen.getByRole('button', { name: 'Confirm delete' }));
// Confirm button should not be visible if closeOnConfirm is true
expect(screen.queryByRole('button', { name: 'Confirm delete' })).not.toBeInTheDocument();
});
it('should show cancel when clicked', () => {
const onCancel = jest.fn();
render(
<ConfirmButton confirmText="Confirm delete" onCancel={onCancel} onConfirm={() => {}}>
Delete
</ConfirmButton>
);
// Cancel button should not be visible before clicking the Delete button
expect(screen.queryByRole('button', { name: 'Cancel' })).not.toBeInTheDocument();
userEvent.click(screen.getByRole('button', { name: 'Delete' }));
// Cancel button should now be visible
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
expect(onCancel).toHaveBeenCalled();
// Cancel button should not be visible after click
expect(screen.queryByRole('button', { name: 'Cancel' })).not.toBeInTheDocument();
});
});
@@ -1,23 +1,34 @@
import React from 'react';
import { mount } from 'enzyme';
import { render, screen, within } from '@testing-library/react';
import { ConfirmModal } from './ConfirmModal';
describe('ConfirmModal', () => {
it('renders without error', () => {
mount(
it('should render correct title, body, dismiss-, alternative- and confirm-text', () => {
render(
<ConfirmModal
title="Some Title"
body="Some Body"
confirmText="Confirm"
confirmText="Please Confirm"
alternativeText="Alternative Text"
dismissText="Dismiss Text"
isOpen={true}
onConfirm={() => {}}
onDismiss={() => {}}
onAlternative={() => {}}
/>
);
expect(screen.getByRole('heading', { name: 'Some Title' })).toBeInTheDocument();
expect(screen.getByText('Some Body')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Dismiss Text' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Alternative Text' })).toBeInTheDocument();
const button = screen.getByRole('button', { name: 'Confirm Modal Danger Button' });
expect(within(button).getByText('Please Confirm')).toBeInTheDocument();
});
it('renders nothing by default or when isOpen is false', () => {
const wrapper = mount(
it('should render nothing when isOpen is false', () => {
render(
<ConfirmModal
title="Some Title"
body="Some Body"
@@ -27,26 +38,11 @@ describe('ConfirmModal', () => {
onDismiss={() => {}}
/>
);
expect(wrapper.html()).toBe('');
wrapper.setProps({ ...wrapper.props(), isOpen: false });
expect(wrapper.html()).toBe('');
});
it('renders correct contents', () => {
const wrapper = mount(
<ConfirmModal
title="Some Title"
body="Content"
confirmText="Confirm"
isOpen={true}
onConfirm={() => {}}
onDismiss={() => {}}
/>
);
expect(wrapper.contains('Some Title')).toBeTruthy();
expect(wrapper.contains('Content')).toBeTruthy();
expect(wrapper.contains('Confirm')).toBeTruthy();
expect(screen.queryByRole('heading', { name: 'Some Title' })).not.toBeInTheDocument();
expect(screen.queryByText('Some Body')).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Dismiss Text' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Alternative Text' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Confirm Modal Danger Button' })).not.toBeInTheDocument();
});
});
@@ -1,12 +1,13 @@
import React, { useEffect, useRef, useState } from 'react';
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Modal } from '../Modal/Modal';
import { IconName } from '../../types/icon';
import { Button } from '../Button';
import { useStyles2 } from '../../themes';
import { GrafanaTheme2 } from '@grafana/data';
import { HorizontalGroup, Input } from '..';
import { selectors } from '@grafana/e2e-selectors';
export interface ConfirmModalProps {
/** Toggle modal's open/closed state */
@@ -9,6 +9,7 @@ import { useStyles2 } from '../../../themes';
export interface RadioButtonGroupProps<T> {
value?: T;
id?: string;
disabled?: boolean;
disabledOptions?: T[];
options: Array<SelectableValue<T>>;
@@ -28,6 +29,7 @@ export function RadioButtonGroup<T>({
disabled,
disabledOptions,
size = 'md',
id,
className,
fullWidth = false,
autoFocus = false,
@@ -52,8 +54,9 @@ export function RadioButtonGroup<T>({
},
[onClick]
);
const id = uniqueId('radiogroup-');
const groupName = useRef(id);
const internalId = id ?? uniqueId('radiogroup-');
const groupName = useRef(internalId);
const styles = useStyles2(getStyles);
const activeButtonRef = useRef<HTMLInputElement | null>(null);
@@ -76,7 +79,7 @@ export function RadioButtonGroup<T>({
aria-label={o.ariaLabel}
onChange={handleOnChange(o)}
onClick={handleOnClick(o)}
id={`option-${o.value}-${id}`}
id={`option-${o.value}-${internalId}`}
name={groupName.current}
description={o.description}
fullWidth={fullWidth}
@@ -69,6 +69,7 @@ function getStyles(theme: GrafanaTheme2) {
transition: opacity 0.3s;
z-index: ${theme.zIndex.tooltip};
max-width: 400px;
overflow-wrap: break-word;
&[data-popper-interactive='false'] {
pointer-events: none;
@@ -23,7 +23,7 @@ export const VizTooltipContainer: React.FC<VizTooltipContainerProps> = ({
position: { x: positionX, y: positionY },
offset: { x: offsetX, y: offsetY },
children,
allowPointerEvents,
allowPointerEvents = false,
className,
...otherProps
}) => {
@@ -18,7 +18,6 @@
"@types/prop-types": "^15",
"@types/react": "17.0.39",
"@types/react-icons": "2.2.7",
"@types/recompose": "0.30.10",
"@types/reselect": "2.2.0",
"@types/tinycolor2": "1.4.3",
"enzyme": "3.11.0",
@@ -46,7 +45,6 @@
"react": "17.0.2",
"react-dom": "17.0.2",
"react-icons": "2.2.7",
"recompose": "0.30.0",
"reselect": "4.1.5",
"tinycolor2": "1.4.2",
"tslib": "2.3.1",
@@ -14,6 +14,7 @@
import React from 'react';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { Popover } from '../common/Popover';
import SpanBar from './SpanBar';
@@ -79,9 +80,13 @@ describe('<SpanBar>', () => {
const { onMouseOver, onMouseLeave } = wrapper.find('[data-test-id="SpanBar--wrapper"]').props();
const labelElm = wrapper.find('[data-test-id="SpanBar--label"]');
expect(labelElm.text()).toBe(shortLabel);
onMouseOver();
act(() => {
onMouseOver();
});
expect(labelElm.text()).toBe(longLabel);
onMouseLeave();
act(() => {
onMouseLeave();
});
expect(labelElm.text()).toBe(shortLabel);
});
@@ -15,8 +15,7 @@
import cx from 'classnames';
import { css } from '@emotion/css';
import { groupBy as _groupBy } from 'lodash';
import React from 'react';
import { compose, onlyUpdateForKeys, withProps, withState } from 'recompose';
import React, { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
import { autoColor } from '../Theme';
@@ -90,7 +89,7 @@ const getStyles = (theme: GrafanaTheme2) => {
};
};
type TCommonProps = {
type Props = {
color: string;
onClick?: (evt: React.MouseEvent<any>) => void;
viewEnd: number;
@@ -107,39 +106,32 @@ type TCommonProps = {
span: TraceSpan;
className?: string;
labelClassName?: string;
};
type TInnerProps = {
label: string;
setLongLabel: () => void;
setShortLabel: () => void;
} & TCommonProps;
type TOuterProps = {
longLabel: string;
shortLabel: string;
} & TCommonProps;
};
function toPercent(value: number) {
return `${(value * 100).toFixed(1)}%`;
}
function SpanBar(props: TInnerProps) {
const {
viewEnd,
viewStart,
getViewedBounds,
color,
label,
onClick,
setLongLabel,
setShortLabel,
rpc,
traceStartTime,
span,
className,
labelClassName,
} = props;
function SpanBar({
viewEnd,
viewStart,
getViewedBounds,
color,
shortLabel,
longLabel,
onClick,
rpc,
traceStartTime,
span,
className,
labelClassName,
}: Props) {
const [label, setLabel] = useState(shortLabel);
const setShortLabel = () => setLabel(shortLabel);
const setLongLabel = () => setLabel(longLabel);
// group logs based on timestamps
const logGroups = _groupBy(span.logs, (log) => {
const posPercent = getViewedBounds(log.timestamp, log.timestamp).start;
@@ -196,21 +188,4 @@ function SpanBar(props: TInnerProps) {
);
}
export default compose<TInnerProps, TOuterProps>(
withState('label', 'setLabel', (props: { shortLabel: string }) => props.shortLabel),
withProps(
({
setLabel,
shortLabel,
longLabel,
}: {
setLabel: (label: string) => void;
shortLabel: string;
longLabel: string;
}) => ({
setLongLabel: () => setLabel(longLabel),
setShortLabel: () => setLabel(shortLabel),
})
),
onlyUpdateForKeys(['label', 'rpc', 'viewStart', 'viewEnd'])
)(SpanBar);
export default React.memo(SpanBar);
@@ -6,9 +6,3 @@ export { default as DetailState } from './TraceTimelineViewer/SpanDetail/DetailS
export { default as transformTraceData } from './model/transform-trace-data';
export { default as filterSpans } from './utils/filter-spans';
export * from './Theme';
import { onlyUpdateForKeys } from 'recompose';
export default {
onlyUpdateForKeys,
} as any;
+126 -97
View File
@@ -1,8 +1,10 @@
package api
import (
"fmt"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/setting"
)
@@ -29,49 +31,49 @@ const (
// API related scopes
var (
ScopeProvisionersAll = accesscontrol.Scope("provisioners", "*")
ScopeProvisionersDashboards = accesscontrol.Scope("provisioners", "dashboards")
ScopeProvisionersPlugins = accesscontrol.Scope("provisioners", "plugins")
ScopeProvisionersDatasources = accesscontrol.Scope("provisioners", "datasources")
ScopeProvisionersNotifications = accesscontrol.Scope("provisioners", "notifications")
ScopeProvisionersAll = ac.Scope("provisioners", "*")
ScopeProvisionersDashboards = ac.Scope("provisioners", "dashboards")
ScopeProvisionersPlugins = ac.Scope("provisioners", "plugins")
ScopeProvisionersDatasources = ac.Scope("provisioners", "datasources")
ScopeProvisionersNotifications = ac.Scope("provisioners", "notifications")
ScopeDatasourcesAll = accesscontrol.Scope("datasources", "*")
ScopeDatasourceID = accesscontrol.Scope("datasources", "id", accesscontrol.Parameter(":id"))
ScopeDatasourceUID = accesscontrol.Scope("datasources", "uid", accesscontrol.Parameter(":uid"))
ScopeDatasourceName = accesscontrol.Scope("datasources", "name", accesscontrol.Parameter(":name"))
ScopeDatasourcesAll = ac.Scope("datasources", "*")
ScopeDatasourceID = ac.Scope("datasources", "id", ac.Parameter(":id"))
ScopeDatasourceUID = ac.Scope("datasources", "uid", ac.Parameter(":uid"))
ScopeDatasourceName = ac.Scope("datasources", "name", ac.Parameter(":name"))
)
// declareFixedRoles declares to the AccessControl service fixed roles and their
// grants to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin"
// that HTTPServer needs
func (hs *HTTPServer) declareFixedRoles() error {
provisioningWriterRole := accesscontrol.RoleRegistration{
Role: accesscontrol.RoleDTO{
provisioningWriterRole := ac.RoleRegistration{
Role: ac.RoleDTO{
Version: 3,
Name: "fixed:provisioning:writer",
DisplayName: "Provisioning writer",
Description: "Reload provisioning.",
Group: "Provisioning",
Permissions: []accesscontrol.Permission{
Permissions: []ac.Permission{
{
Action: ActionProvisioningReload,
Scope: ScopeProvisionersAll,
},
},
},
Grants: []string{accesscontrol.RoleGrafanaAdmin},
Grants: []string{ac.RoleGrafanaAdmin},
}
datasourcesExplorerRole := accesscontrol.RoleRegistration{
Role: accesscontrol.RoleDTO{
datasourcesExplorerRole := ac.RoleRegistration{
Role: ac.RoleDTO{
Version: 4,
Name: "fixed:datasources:explorer",
DisplayName: "Data source explorer",
Description: "Enable the Explore feature. Data source permissions still apply; you can only query data sources for which you have query permissions.",
Group: "Data sources",
Permissions: []accesscontrol.Permission{
Permissions: []ac.Permission{
{
Action: accesscontrol.ActionDatasourcesExplore,
Action: ac.ActionDatasourcesExplore,
},
},
},
@@ -82,14 +84,14 @@ func (hs *HTTPServer) declareFixedRoles() error {
datasourcesExplorerRole.Grants = append(datasourcesExplorerRole.Grants, string(models.ROLE_VIEWER))
}
datasourcesReaderRole := accesscontrol.RoleRegistration{
Role: accesscontrol.RoleDTO{
datasourcesReaderRole := ac.RoleRegistration{
Role: ac.RoleDTO{
Version: 3,
Name: "fixed:datasources:reader",
DisplayName: "Data source reader",
Description: "Read and query all data sources.",
Group: "Data sources",
Permissions: []accesscontrol.Permission{
Permissions: []ac.Permission{
{
Action: ActionDatasourcesRead,
Scope: ScopeDatasourcesAll,
@@ -103,14 +105,14 @@ func (hs *HTTPServer) declareFixedRoles() error {
Grants: []string{string(models.ROLE_ADMIN)},
}
datasourcesWriterRole := accesscontrol.RoleRegistration{
Role: accesscontrol.RoleDTO{
datasourcesWriterRole := ac.RoleRegistration{
Role: ac.RoleDTO{
Version: 3,
Name: "fixed:datasources:writer",
DisplayName: "Data source writer",
Description: "Create, update, delete, read, or query data sources.",
Group: "Data sources",
Permissions: accesscontrol.ConcatPermissions(datasourcesReaderRole.Role.Permissions, []accesscontrol.Permission{
Permissions: ac.ConcatPermissions(datasourcesReaderRole.Role.Permissions, []ac.Permission{
{
Action: ActionDatasourcesWrite,
Scope: ScopeDatasourcesAll,
@@ -127,14 +129,14 @@ func (hs *HTTPServer) declareFixedRoles() error {
Grants: []string{string(models.ROLE_ADMIN)},
}
datasourcesIdReaderRole := accesscontrol.RoleRegistration{
Role: accesscontrol.RoleDTO{
datasourcesIdReaderRole := ac.RoleRegistration{
Role: ac.RoleDTO{
Version: 4,
Name: "fixed:datasources.id:reader",
DisplayName: "Data source ID reader",
Description: "Read the ID of a data source based on its name.",
Group: "Infrequently used",
Permissions: []accesscontrol.Permission{
Permissions: []ac.Permission{
{
Action: ActionDatasourcesIDRead,
Scope: ScopeDatasourcesAll,
@@ -144,14 +146,14 @@ func (hs *HTTPServer) declareFixedRoles() error {
Grants: []string{string(models.ROLE_VIEWER)},
}
datasourcesCompatibilityReaderRole := accesscontrol.RoleRegistration{
Role: accesscontrol.RoleDTO{
datasourcesCompatibilityReaderRole := ac.RoleRegistration{
Role: ac.RoleDTO{
Version: 3,
Name: "fixed:datasources:compatibility:querier",
DisplayName: "Data source compatibility querier",
Description: "Only used for open source compatibility. Query data sources.",
Group: "Infrequently used",
Permissions: []accesscontrol.Permission{
Permissions: []ac.Permission{
{Action: ActionDatasourcesQuery},
{Action: ActionDatasourcesRead},
},
@@ -159,29 +161,29 @@ func (hs *HTTPServer) declareFixedRoles() error {
Grants: []string{string(models.ROLE_VIEWER)},
}
orgReaderRole := accesscontrol.RoleRegistration{
Role: accesscontrol.RoleDTO{
orgReaderRole := ac.RoleRegistration{
Role: ac.RoleDTO{
Version: 5,
Name: "fixed:organization:reader",
DisplayName: "Organization reader",
Description: "Read an organization, such as its ID, name, address, or quotas.",
Group: "Organizations",
Permissions: []accesscontrol.Permission{
Permissions: []ac.Permission{
{Action: ActionOrgsRead},
{Action: ActionOrgsQuotasRead},
},
},
Grants: []string{string(models.ROLE_VIEWER), accesscontrol.RoleGrafanaAdmin},
Grants: []string{string(models.ROLE_VIEWER), ac.RoleGrafanaAdmin},
}
orgWriterRole := accesscontrol.RoleRegistration{
Role: accesscontrol.RoleDTO{
orgWriterRole := ac.RoleRegistration{
Role: ac.RoleDTO{
Version: 5,
Name: "fixed:organization:writer",
DisplayName: "Organization writer",
Description: "Read an organization, its quotas, or its preferences. Update organization properties, or its preferences.",
Group: "Organizations",
Permissions: accesscontrol.ConcatPermissions(orgReaderRole.Role.Permissions, []accesscontrol.Permission{
Permissions: ac.ConcatPermissions(orgReaderRole.Role.Permissions, []ac.Permission{
{Action: ActionOrgsPreferencesRead},
{Action: ActionOrgsWrite},
{Action: ActionOrgsPreferencesWrite},
@@ -190,71 +192,71 @@ func (hs *HTTPServer) declareFixedRoles() error {
Grants: []string{string(models.ROLE_ADMIN)},
}
orgMaintainerRole := accesscontrol.RoleRegistration{
Role: accesscontrol.RoleDTO{
orgMaintainerRole := ac.RoleRegistration{
Role: ac.RoleDTO{
Version: 5,
Name: "fixed:organization:maintainer",
DisplayName: "Organization maintainer",
Description: "Create, read, write, or delete an organization. Read or write an organization's quotas. Needs to be assigned globally.",
Group: "Organizations",
Permissions: accesscontrol.ConcatPermissions(orgReaderRole.Role.Permissions, []accesscontrol.Permission{
Permissions: ac.ConcatPermissions(orgReaderRole.Role.Permissions, []ac.Permission{
{Action: ActionOrgsCreate},
{Action: ActionOrgsWrite},
{Action: ActionOrgsDelete},
{Action: ActionOrgsQuotasWrite},
}),
},
Grants: []string{string(accesscontrol.RoleGrafanaAdmin)},
Grants: []string{string(ac.RoleGrafanaAdmin)},
}
teamCreatorGrants := []string{string(models.ROLE_ADMIN)}
if hs.Cfg.EditorsCanAdmin {
teamCreatorGrants = append(teamCreatorGrants, string(models.ROLE_EDITOR))
}
teamsCreatorRole := accesscontrol.RoleRegistration{
Role: accesscontrol.RoleDTO{
teamsCreatorRole := ac.RoleRegistration{
Role: ac.RoleDTO{
Name: "fixed:teams:creator",
DisplayName: "Team creator",
Description: "Create teams and read organisation users (required to manage the created teams).",
Group: "Teams",
Version: 2,
Permissions: []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsCreate},
{Action: accesscontrol.ActionOrgUsersRead, Scope: accesscontrol.ScopeUsersAll},
Permissions: []ac.Permission{
{Action: ac.ActionTeamsCreate},
{Action: ac.ActionOrgUsersRead, Scope: ac.ScopeUsersAll},
},
},
Grants: teamCreatorGrants,
}
teamsWriterRole := accesscontrol.RoleRegistration{
Role: accesscontrol.RoleDTO{
teamsWriterRole := ac.RoleRegistration{
Role: ac.RoleDTO{
Name: "fixed:teams:writer",
DisplayName: "Team writer",
Description: "Create, read, write, or delete a team as well as controlling team memberships.",
Group: "Teams",
Version: 2,
Permissions: []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsCreate},
{Action: accesscontrol.ActionTeamsDelete, Scope: accesscontrol.ScopeTeamsAll},
{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: accesscontrol.ScopeTeamsAll},
{Action: accesscontrol.ActionTeamsPermissionsWrite, Scope: accesscontrol.ScopeTeamsAll},
{Action: accesscontrol.ActionTeamsRead, Scope: accesscontrol.ScopeTeamsAll},
{Action: accesscontrol.ActionTeamsWrite, Scope: accesscontrol.ScopeTeamsAll},
Permissions: []ac.Permission{
{Action: ac.ActionTeamsCreate},
{Action: ac.ActionTeamsDelete, Scope: ac.ScopeTeamsAll},
{Action: ac.ActionTeamsPermissionsRead, Scope: ac.ScopeTeamsAll},
{Action: ac.ActionTeamsPermissionsWrite, Scope: ac.ScopeTeamsAll},
{Action: ac.ActionTeamsRead, Scope: ac.ScopeTeamsAll},
{Action: ac.ActionTeamsWrite, Scope: ac.ScopeTeamsAll},
},
},
Grants: []string{string(models.ROLE_ADMIN)},
}
annotationsReaderRole := accesscontrol.RoleRegistration{
Role: accesscontrol.RoleDTO{
annotationsReaderRole := ac.RoleRegistration{
Role: ac.RoleDTO{
Name: "fixed:annotations:reader",
DisplayName: "Annotation reader",
Description: "Read annotations and tags",
Group: "Annotations",
Version: 1,
Permissions: []accesscontrol.Permission{
{Action: accesscontrol.ActionAnnotationsRead, Scope: accesscontrol.ScopeAnnotationsAll},
{Action: accesscontrol.ActionAnnotationsTagsRead, Scope: accesscontrol.ScopeAnnotationsTagsAll},
Permissions: []ac.Permission{
{Action: ac.ActionAnnotationsRead, Scope: ac.ScopeAnnotationsAll},
{Action: ac.ActionAnnotationsTagsRead, Scope: ac.ScopeAnnotationsTagsAll},
},
},
Grants: []string{string(models.ROLE_VIEWER)},
@@ -271,69 +273,96 @@ func (hs *HTTPServer) declareFixedRoles() error {
// here is the list of complex evaluators we use in this package
// dataSourcesConfigurationAccessEvaluator is used to protect the "Configure > Data sources" tab access
var dataSourcesConfigurationAccessEvaluator = accesscontrol.EvalAll(
accesscontrol.EvalPermission(ActionDatasourcesRead),
accesscontrol.EvalAny(
accesscontrol.EvalPermission(ActionDatasourcesCreate),
accesscontrol.EvalPermission(ActionDatasourcesDelete),
accesscontrol.EvalPermission(ActionDatasourcesWrite),
var dataSourcesConfigurationAccessEvaluator = ac.EvalAll(
ac.EvalPermission(ActionDatasourcesRead),
ac.EvalAny(
ac.EvalPermission(ActionDatasourcesCreate),
ac.EvalPermission(ActionDatasourcesDelete),
ac.EvalPermission(ActionDatasourcesWrite),
),
)
// dataSourcesNewAccessEvaluator is used to protect the "Configure > Data sources > New" page access
var dataSourcesNewAccessEvaluator = accesscontrol.EvalAll(
accesscontrol.EvalPermission(ActionDatasourcesRead),
accesscontrol.EvalPermission(ActionDatasourcesCreate),
accesscontrol.EvalPermission(ActionDatasourcesWrite),
var dataSourcesNewAccessEvaluator = ac.EvalAll(
ac.EvalPermission(ActionDatasourcesRead),
ac.EvalPermission(ActionDatasourcesCreate),
ac.EvalPermission(ActionDatasourcesWrite),
)
// dataSourcesEditAccessEvaluator is used to protect the "Configure > Data sources > Edit" page access
var dataSourcesEditAccessEvaluator = accesscontrol.EvalAll(
accesscontrol.EvalPermission(ActionDatasourcesRead),
accesscontrol.EvalPermission(ActionDatasourcesWrite),
var dataSourcesEditAccessEvaluator = ac.EvalAll(
ac.EvalPermission(ActionDatasourcesRead),
ac.EvalPermission(ActionDatasourcesWrite),
)
// orgPreferencesAccessEvaluator is used to protect the "Configure > Preferences" page access
var orgPreferencesAccessEvaluator = accesscontrol.EvalAny(
accesscontrol.EvalAll(
accesscontrol.EvalPermission(ActionOrgsRead),
accesscontrol.EvalPermission(ActionOrgsWrite),
var orgPreferencesAccessEvaluator = ac.EvalAny(
ac.EvalAll(
ac.EvalPermission(ActionOrgsRead),
ac.EvalPermission(ActionOrgsWrite),
),
accesscontrol.EvalAll(
accesscontrol.EvalPermission(ActionOrgsPreferencesRead),
accesscontrol.EvalPermission(ActionOrgsPreferencesWrite),
ac.EvalAll(
ac.EvalPermission(ActionOrgsPreferencesRead),
ac.EvalPermission(ActionOrgsPreferencesWrite),
),
)
// orgsAccessEvaluator is used to protect the "Server Admin > Orgs" page access
// (you need to have read access to update or delete orgs; read is the minimum)
var orgsAccessEvaluator = accesscontrol.EvalPermission(ActionOrgsRead)
var orgsAccessEvaluator = ac.EvalPermission(ActionOrgsRead)
// orgsCreateAccessEvaluator is used to protect the "Server Admin > Orgs > New Org" page access
var orgsCreateAccessEvaluator = accesscontrol.EvalAll(
accesscontrol.EvalPermission(ActionOrgsRead),
accesscontrol.EvalPermission(ActionOrgsCreate),
var orgsCreateAccessEvaluator = ac.EvalAll(
ac.EvalPermission(ActionOrgsRead),
ac.EvalPermission(ActionOrgsCreate),
)
// teamsAccessEvaluator is used to protect the "Configuration > Teams" page access
// grants access to a user when they can either create teams or can read and update a team
var teamsAccessEvaluator = accesscontrol.EvalAny(
accesscontrol.EvalPermission(accesscontrol.ActionTeamsCreate),
accesscontrol.EvalAll(
accesscontrol.EvalPermission(accesscontrol.ActionTeamsRead),
accesscontrol.EvalAny(
accesscontrol.EvalPermission(accesscontrol.ActionTeamsWrite),
accesscontrol.EvalPermission(accesscontrol.ActionTeamsPermissionsWrite),
var teamsAccessEvaluator = ac.EvalAny(
ac.EvalPermission(ac.ActionTeamsCreate),
ac.EvalAll(
ac.EvalPermission(ac.ActionTeamsRead),
ac.EvalAny(
ac.EvalPermission(ac.ActionTeamsWrite),
ac.EvalPermission(ac.ActionTeamsPermissionsWrite),
),
),
)
// teamsEditAccessEvaluator is used to protect the "Configuration > Teams > edit" page access
var teamsEditAccessEvaluator = accesscontrol.EvalAll(
accesscontrol.EvalPermission(accesscontrol.ActionTeamsRead),
accesscontrol.EvalAny(
accesscontrol.EvalPermission(accesscontrol.ActionTeamsCreate),
accesscontrol.EvalPermission(accesscontrol.ActionTeamsWrite),
accesscontrol.EvalPermission(accesscontrol.ActionTeamsPermissionsWrite),
var teamsEditAccessEvaluator = ac.EvalAll(
ac.EvalPermission(ac.ActionTeamsRead),
ac.EvalAny(
ac.EvalPermission(ac.ActionTeamsCreate),
ac.EvalPermission(ac.ActionTeamsWrite),
ac.EvalPermission(ac.ActionTeamsPermissionsWrite),
),
)
// Metadata helpers
// getAccessControlMetadata returns the accesscontrol metadata associated with a given resource
func (hs *HTTPServer) getAccessControlMetadata(c *models.ReqContext, resource string, id int64) ac.Metadata {
key := fmt.Sprintf("%d", id)
ids := map[string]bool{key: true}
return hs.getMultiAccessControlMetadata(c, resource, ids)[key]
}
// getMultiAccessControlMetadata returns the accesscontrol metadata associated with a given set of resources
func (hs *HTTPServer) getMultiAccessControlMetadata(c *models.ReqContext, resource string, ids map[string]bool) map[string]ac.Metadata {
if hs.AccessControl.IsDisabled() || !c.QueryBool("accesscontrol") {
return map[string]ac.Metadata{}
}
if c.SignedInUser.Permissions == nil {
return map[string]ac.Metadata{}
}
permissions, ok := c.SignedInUser.Permissions[c.OrgId]
if !ok {
return map[string]ac.Metadata{}
}
return ac.GetResourcesMetadata(c.Req.Context(), permissions, resource, ids)
}
-15
View File
@@ -1,15 +0,0 @@
package api
import (
"context"
"github.com/grafana/grafana/pkg/dashboards"
"github.com/grafana/grafana/pkg/models"
)
// updateDashboardACL updates a dashboard's ACL items.
//
// Stubbable by tests.
var updateDashboardACL = func(ctx context.Context, s dashboards.Store, dashID int64, items []*models.DashboardAcl) error {
return s.UpdateDashboardACLCtx(ctx, dashID, items)
}
+12 -12
View File
@@ -239,7 +239,7 @@ func (hs *HTTPServer) GetAlertNotifications(c *models.ReqContext) response.Respo
func (hs *HTTPServer) getAlertNotificationsInternal(c *models.ReqContext) ([]*models.AlertNotification, error) {
query := &models.GetAllAlertNotificationsQuery{OrgId: c.OrgId}
if err := hs.SQLStore.GetAllAlertNotifications(c.Req.Context(), query); err != nil {
if err := hs.AlertNotificationService.GetAllAlertNotifications(c.Req.Context(), query); err != nil {
return nil, err
}
@@ -260,7 +260,7 @@ func (hs *HTTPServer) GetAlertNotificationByID(c *models.ReqContext) response.Re
return response.Error(404, "Alert notification not found", nil)
}
if err := hs.SQLStore.GetAlertNotifications(c.Req.Context(), query); err != nil {
if err := hs.AlertNotificationService.GetAlertNotifications(c.Req.Context(), query); err != nil {
return response.Error(500, "Failed to get alert notifications", err)
}
@@ -281,7 +281,7 @@ func (hs *HTTPServer) GetAlertNotificationByUID(c *models.ReqContext) response.R
return response.Error(404, "Alert notification not found", nil)
}
if err := hs.SQLStore.GetAlertNotificationsWithUid(c.Req.Context(), query); err != nil {
if err := hs.AlertNotificationService.GetAlertNotificationsWithUid(c.Req.Context(), query); err != nil {
return response.Error(500, "Failed to get alert notifications", err)
}
@@ -299,7 +299,7 @@ func (hs *HTTPServer) CreateAlertNotification(c *models.ReqContext) response.Res
}
cmd.OrgId = c.OrgId
if err := hs.SQLStore.CreateAlertNotificationCommand(c.Req.Context(), &cmd); err != nil {
if err := hs.AlertNotificationService.CreateAlertNotificationCommand(c.Req.Context(), &cmd); err != nil {
if errors.Is(err, models.ErrAlertNotificationWithSameNameExists) || errors.Is(err, models.ErrAlertNotificationWithSameUIDExists) {
return response.Error(409, "Failed to create alert notification", err)
}
@@ -325,7 +325,7 @@ func (hs *HTTPServer) UpdateAlertNotification(c *models.ReqContext) response.Res
return response.Error(500, "Failed to update alert notification", err)
}
if err := hs.SQLStore.UpdateAlertNotification(c.Req.Context(), &cmd); err != nil {
if err := hs.AlertNotificationService.UpdateAlertNotification(c.Req.Context(), &cmd); err != nil {
if errors.Is(err, models.ErrAlertNotificationNotFound) {
return response.Error(404, err.Error(), err)
}
@@ -341,7 +341,7 @@ func (hs *HTTPServer) UpdateAlertNotification(c *models.ReqContext) response.Res
Id: cmd.Id,
}
if err := hs.SQLStore.GetAlertNotifications(c.Req.Context(), &query); err != nil {
if err := hs.AlertNotificationService.GetAlertNotifications(c.Req.Context(), &query); err != nil {
return response.Error(500, "Failed to get alert notification", err)
}
@@ -361,7 +361,7 @@ func (hs *HTTPServer) UpdateAlertNotificationByUID(c *models.ReqContext) respons
return response.Error(500, "Failed to update alert notification", err)
}
if err := hs.SQLStore.UpdateAlertNotificationWithUid(c.Req.Context(), &cmd); err != nil {
if err := hs.AlertNotificationService.UpdateAlertNotificationWithUid(c.Req.Context(), &cmd); err != nil {
if errors.Is(err, models.ErrAlertNotificationNotFound) {
return response.Error(404, err.Error(), nil)
}
@@ -373,7 +373,7 @@ func (hs *HTTPServer) UpdateAlertNotificationByUID(c *models.ReqContext) respons
Uid: cmd.Uid,
}
if err := hs.SQLStore.GetAlertNotificationsWithUid(c.Req.Context(), &query); err != nil {
if err := hs.AlertNotificationService.GetAlertNotificationsWithUid(c.Req.Context(), &query); err != nil {
return response.Error(500, "Failed to get alert notification", err)
}
@@ -390,7 +390,7 @@ func (hs *HTTPServer) fillWithSecureSettingsData(ctx context.Context, cmd *model
Id: cmd.Id,
}
if err := hs.SQLStore.GetAlertNotifications(ctx, query); err != nil {
if err := hs.AlertNotificationService.GetAlertNotifications(ctx, query); err != nil {
return err
}
@@ -418,7 +418,7 @@ func (hs *HTTPServer) fillWithSecureSettingsDataByUID(ctx context.Context, cmd *
Uid: cmd.Uid,
}
if err := hs.SQLStore.GetAlertNotificationsWithUid(ctx, query); err != nil {
if err := hs.AlertNotificationService.GetAlertNotificationsWithUid(ctx, query); err != nil {
return err
}
@@ -447,7 +447,7 @@ func (hs *HTTPServer) DeleteAlertNotification(c *models.ReqContext) response.Res
Id: notificationId,
}
if err := hs.SQLStore.DeleteAlertNotification(c.Req.Context(), &cmd); err != nil {
if err := hs.AlertNotificationService.DeleteAlertNotification(c.Req.Context(), &cmd); err != nil {
if errors.Is(err, models.ErrAlertNotificationNotFound) {
return response.Error(404, err.Error(), nil)
}
@@ -463,7 +463,7 @@ func (hs *HTTPServer) DeleteAlertNotificationByUID(c *models.ReqContext) respons
Uid: web.Params(c.Req)[":uid"],
}
if err := hs.SQLStore.DeleteAlertNotificationWithUid(c.Req.Context(), &cmd); err != nil {
if err := hs.AlertNotificationService.DeleteAlertNotificationWithUid(c.Req.Context(), &cmd); err != nil {
if errors.Is(err, models.ErrAlertNotificationNotFound) {
return response.Error(404, err.Error(), nil)
}
+7 -4
View File
@@ -26,7 +26,7 @@ func (hs *HTTPServer) registerRoutes() {
reqGrafanaAdmin := middleware.ReqGrafanaAdmin
reqEditorRole := middleware.ReqEditorRole
reqOrgAdmin := middleware.ReqOrgAdmin
reqOrgAdminFolderAdminOrTeamAdmin := middleware.OrgAdminFolderAdminOrTeamAdmin
reqOrgAdminFolderAdminOrTeamAdmin := middleware.OrgAdminFolderAdminOrTeamAdmin(hs.SQLStore)
reqCanAccessTeams := middleware.AdminOrEditorAndFeatureEnabled(hs.Cfg.EditorsCanAdmin)
reqSnapshotPublicModeOrSignedIn := middleware.SnapshotPublicModeOrSignedIn(hs.Cfg)
redirectFromLegacyPanelEditURL := middleware.RedirectFromLegacyPanelEditURL(hs.Cfg)
@@ -336,8 +336,11 @@ func (hs *HTTPServer) registerRoutes() {
if hs.ThumbService != nil {
dashboardRoute.Get("/uid/:uid/img/:kind/:theme", hs.ThumbService.GetImage)
dashboardRoute.Post("/uid/:uid/img/:kind/:theme", hs.ThumbService.SetImage)
dashboardRoute.Put("/uid/:uid/img/:kind/:theme", hs.ThumbService.UpdateThumbnailState)
if hs.Features.IsEnabled(featuremgmt.FlagDashboardPreviewsAdmin) {
dashboardRoute.Post("/uid/:uid/img/:kind/:theme", reqGrafanaAdmin, hs.ThumbService.SetImage)
dashboardRoute.Put("/uid/:uid/img/:kind/:theme", reqGrafanaAdmin, hs.ThumbService.UpdateThumbnailState)
}
}
dashboardRoute.Post("/calculate-diff", routing.Wrap(hs.CalculateDashboardDiff))
@@ -470,7 +473,7 @@ func (hs *HTTPServer) registerRoutes() {
adminRoute.Get("/stats", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionServerStatsRead)), routing.Wrap(hs.AdminGetStats))
adminRoute.Post("/pause-all-alerts", reqGrafanaAdmin, routing.Wrap(hs.PauseAllAlerts))
if hs.ThumbService != nil {
if hs.ThumbService != nil && hs.Features.IsEnabled(featuremgmt.FlagDashboardPreviewsAdmin) {
adminRoute.Post("/crawler/start", reqGrafanaAdmin, routing.Wrap(hs.ThumbService.StartCrawler))
adminRoute.Post("/crawler/stop", reqGrafanaAdmin, routing.Wrap(hs.ThumbService.StopCrawler))
adminRoute.Get("/crawler/status", reqGrafanaAdmin, routing.Wrap(hs.ThumbService.CrawlerStatus))
+19 -11
View File
@@ -25,9 +25,11 @@ import (
acmiddleware "github.com/grafana/grafana/pkg/services/accesscontrol/middleware"
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourceservices"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/contexthandler"
"github.com/grafana/grafana/pkg/services/dashboards"
dashboardsstore "github.com/grafana/grafana/pkg/services/dashboards/database"
dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/manager"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ldap"
"github.com/grafana/grafana/pkg/services/quota"
@@ -276,6 +278,8 @@ type accessControlScenarioContext struct {
// cfg is the setting provider
cfg *setting.Cfg
dashboardsStore dashboards.Store
}
func setAccessControlPermissions(acmock *accesscontrolmock.Mock, perms []*accesscontrol.Permission, org int64) {
@@ -347,6 +351,8 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont
bus := bus.GetBus()
dashboardsStore := dashboardsstore.ProvideDashboardStore(db)
routeRegister := routing.NewRouteRegister()
// Create minimal HTTP Server
hs := &HTTPServer{
@@ -358,6 +364,7 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont
RouteRegister: routeRegister,
SQLStore: db,
searchUsersService: searchusers.ProvideUsersService(bus, filters.ProvideOSSSearchUserFilter()),
dashboardService: dashboardservice.ProvideDashboardService(dashboardsStore),
}
// Defining the accesscontrol service has to be done before registering routes
@@ -367,9 +374,9 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont
acmock = acmock.WithDisabled()
}
hs.AccessControl = acmock
teamPermissionService, err := resourceservices.ProvideTeamPermissions(routeRegister, db, acmock, database.ProvideService(db))
teamPermissionService, err := ossaccesscontrol.ProvideTeamPermissions(routeRegister, db, acmock, database.ProvideService(db))
require.NoError(t, err)
hs.TeamPermissionsService = teamPermissionService
hs.teamPermissionsService = teamPermissionService
} else {
ac := ossaccesscontrol.ProvideService(hs.Features, &usagestats.UsageStatsMock{T: t},
database.ProvideService(db), routing.NewRouteRegister())
@@ -379,9 +386,9 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont
require.NoError(t, err)
err = ac.RegisterFixedRoles()
require.NoError(t, err)
teamPermissionService, err := resourceservices.ProvideTeamPermissions(routeRegister, db, ac, database.ProvideService(db))
teamPermissionService, err := ossaccesscontrol.ProvideTeamPermissions(routeRegister, db, ac, database.ProvideService(db))
require.NoError(t, err)
hs.TeamPermissionsService = teamPermissionService
hs.teamPermissionsService = teamPermissionService
}
// Instantiate a new Server
@@ -402,12 +409,13 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont
hs.RouteRegister.Register(m.Router)
return accessControlScenarioContext{
server: m,
initCtx: initCtx,
hs: hs,
acmock: acmock,
db: db,
cfg: cfg,
server: m,
initCtx: initCtx,
hs: hs,
acmock: acmock,
db: db,
cfg: cfg,
dashboardsStore: dashboardsStore,
}
}
+10 -14
View File
@@ -148,8 +148,7 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response {
meta.FolderUrl = query.Result.GetUrl()
}
svc := dashboards.NewProvisioningService(hs.SQLStore)
provisioningData, err := svc.GetProvisionedDashboardDataByDashboardID(dash.Id)
provisioningData, err := hs.dashboardProvisioningService.GetProvisionedDashboardDataByDashboardID(dash.Id)
if err != nil {
return response.Error(500, "Error while checking if dashboard is provisioned", err)
}
@@ -233,8 +232,8 @@ func (hs *HTTPServer) deleteDashboard(c *models.ReqContext) response.Response {
if err != nil {
hs.log.Error("Failed to disconnect library elements", "dashboard", dash.Id, "user", c.SignedInUser.UserId, "error", err)
}
svc := dashboards.NewService(hs.SQLStore)
err = svc.DeleteDashboard(c.Req.Context(), dash.Id, c.OrgId)
err = hs.dashboardService.DeleteDashboard(c.Req.Context(), dash.Id, c.OrgId)
if err != nil {
var dashboardErr models.DashboardErr
if ok := errors.As(err, &dashboardErr); ok {
@@ -271,8 +270,7 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa
cmd.OrgId = c.OrgId
cmd.UserId = c.UserId
if cmd.FolderUid != "" {
folders := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore)
folder, err := folders.GetFolderByUID(ctx, cmd.FolderUid)
folder, err := hs.folderService.GetFolderByUID(ctx, c.SignedInUser, c.OrgId, cmd.FolderUid)
if err != nil {
if errors.Is(err, models.ErrFolderNotFound) {
return response.Error(400, "Folder not found", err)
@@ -294,18 +292,17 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa
}
}
svc := dashboards.NewProvisioningService(hs.SQLStore)
var provisioningData *models.DashboardProvisioning
if dash.Id != 0 {
data, err := svc.GetProvisionedDashboardDataByDashboardID(dash.Id)
data, err := hs.dashboardProvisioningService.GetProvisionedDashboardDataByDashboardID(dash.Id)
if err != nil {
return response.Error(500, "Error while checking if dashboard is provisioned using ID", err)
}
provisioningData = data
} else if dash.Uid != "" {
data, err := svc.GetProvisionedDashboardDataByDashboardUID(dash.OrgId, dash.Uid)
if err != nil && (!errors.Is(err, models.ErrProvisionedDashboardNotFound) && !errors.Is(err, models.ErrDashboardNotFound)) {
return response.Error(500, "Error while checking if dashboard is provisioned using UID", err)
data, err := hs.dashboardProvisioningService.GetProvisionedDashboardDataByDashboardUID(dash.OrgId, dash.Uid)
if err != nil && !errors.Is(err, models.ErrProvisionedDashboardNotFound) && !errors.Is(err, models.ErrDashboardNotFound) {
return response.Error(500, "Error while checking if dashboard is provisioned", err)
}
provisioningData = data
}
@@ -329,8 +326,7 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa
Overwrite: cmd.Overwrite,
}
dashSvc := dashboards.NewService(hs.SQLStore)
dashboard, err := dashSvc.SaveDashboard(alerting.WithUAEnabled(ctx, hs.Cfg.UnifiedAlerting.IsEnabled()), dashItem, allowUiUpdate)
dashboard, err := hs.dashboardService.SaveDashboard(alerting.WithUAEnabled(ctx, hs.Cfg.UnifiedAlerting.IsEnabled()), dashItem, allowUiUpdate)
if hs.Live != nil {
// Tell everyone listening that the dashboard changed
@@ -362,7 +358,7 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa
if hs.Cfg.EditorsCanAdmin && newDashboard {
inFolder := cmd.FolderId > 0
err := dashSvc.MakeUserAdmin(ctx, cmd.OrgId, cmd.UserId, dashboard.Id, !inFolder)
err := hs.dashboardService.MakeUserAdmin(ctx, cmd.OrgId, cmd.UserId, dashboard.Id, !inFolder)
if err != nil {
hs.log.Error("Could not make user admin", "dashboard", dashboard.Title, "user", c.SignedInUser.UserId, "error", err)
}
+1 -1
View File
@@ -112,7 +112,7 @@ func (hs *HTTPServer) UpdateDashboardPermissions(c *models.ReqContext) response.
return response.Error(403, "Cannot remove own admin permission for a folder", nil)
}
if err := updateDashboardACL(c.Req.Context(), hs.SQLStore, dashID, items); err != nil {
if err := hs.dashboardService.UpdateDashboardACL(c.Req.Context(), dashID, items); err != nil {
if errors.Is(err, models.ErrDashboardAclInfoMissing) ||
errors.Is(err, models.ErrDashboardPermissionDashboardEmpty) {
return response.Error(409, err.Error(), err)
+19 -55
View File
@@ -1,59 +1,38 @@
package api
import (
"context"
"encoding/json"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/dashboards"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards/database"
dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/manager"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func TestDashboardPermissionAPIEndpoint(t *testing.T) {
t.Run("Dashboard permissions test", func(t *testing.T) {
settings := setting.NewCfg()
dashboardStore := &database.FakeDashboardStore{}
defer dashboardStore.AssertExpectations(t)
mockSQLStore := mockstore.NewSQLStoreMock()
hs := &HTTPServer{
Cfg: settings,
SQLStore: mockSQLStore,
Cfg: settings,
dashboardService: dashboardservice.ProvideDashboardService(dashboardStore),
SQLStore: mockSQLStore,
}
t.Run("Given dashboard not exists", func(t *testing.T) {
mockSQLStore.ExpectedError = models.ErrDashboardNotFound
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/1/permissions",
"/api/dashboards/id/:dashboardId/permissions", models.ROLE_EDITOR, func(sc *scenarioContext) {
callGetDashboardPermissions(sc, hs)
assert.Equal(t, 404, sc.resp.Code)
}, mockSQLStore)
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
{UserID: 1000, Permission: models.PERMISSION_ADMIN},
},
}
updateDashboardPermissionScenario(t, updatePermissionContext{
desc: "When calling POST on",
url: "/api/dashboards/id/1/permissions",
routePattern: "/api/dashboards/id/:dashboardId/permissions",
cmd: cmd,
fn: func(sc *scenarioContext) {
callUpdateDashboardPermissions(t, sc)
assert.Equal(t, 404, sc.resp.Code)
},
}, hs)
})
t.Run("Given user has no admin permissions", func(t *testing.T) {
origNewGuardian := guardian.New
t.Cleanup(func() {
@@ -63,7 +42,6 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) {
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanAdminValue: false})
getDashboardQueryResult := models.NewDashboard("Dash")
mockSQLStore := mockstore.NewSQLStoreMock()
mockSQLStore.ExpectedDashboard = getDashboardQueryResult
mockSQLStore.ExpectedError = nil
@@ -80,6 +58,7 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) {
},
}
dashboardStore.On("UpdateDashboardACL", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
updateDashboardPermissionScenario(t, updatePermissionContext{
desc: "When calling POST on",
url: "/api/dashboards/id/1/permissions",
@@ -324,26 +303,20 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) {
}
assert.Len(t, cmd.Items, 3)
var numOfItems []*models.DashboardAcl
dashboardStore.On("UpdateDashboardACL", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
items := args.Get(2).([]*models.DashboardAcl)
numOfItems = items
}).Return(nil).Once()
updateDashboardPermissionScenario(t, updatePermissionContext{
desc: "When calling POST on",
url: "/api/dashboards/id/1/permissions",
routePattern: "/api/dashboards/id/:dashboardId/permissions",
cmd: cmd,
fn: func(sc *scenarioContext) {
// TODO: Replace this fake with a fake SQLStore instead (once we can use an interface in its stead)
origUpdateDashboardACL := updateDashboardACL
t.Cleanup(func() {
updateDashboardACL = origUpdateDashboardACL
})
var gotItems []*models.DashboardAcl
updateDashboardACL = func(_ context.Context, _ dashboards.Store, folderID int64, items []*models.DashboardAcl) error {
gotItems = items
return nil
}
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
assert.Len(t, gotItems, 4)
assert.Len(t, numOfItems, 4)
},
}, hs)
})
@@ -357,15 +330,6 @@ func callGetDashboardPermissions(sc *scenarioContext, hs *HTTPServer) {
func callUpdateDashboardPermissions(t *testing.T, sc *scenarioContext) {
t.Helper()
origUpdateDashboardACL := updateDashboardACL
t.Cleanup(func() {
updateDashboardACL = origUpdateDashboardACL
})
updateDashboardACL = func(_ context.Context, _ dashboards.Store, dashID int64, items []*models.DashboardAcl) error {
return nil
}
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
}
+7 -8
View File
@@ -133,7 +133,7 @@ func (hs *HTTPServer) CreateDashboardSnapshot(c *models.ReqContext) response.Res
metrics.MApiDashboardSnapshotCreate.Inc()
}
if err := hs.SQLStore.CreateDashboardSnapshot(c.Req.Context(), &cmd); err != nil {
if err := hs.DashboardsnapshotsService.CreateDashboardSnapshot(c.Req.Context(), &cmd); err != nil {
c.JsonApiErr(500, "Failed to create snapshot", err)
return nil
}
@@ -157,7 +157,7 @@ func (hs *HTTPServer) GetDashboardSnapshot(c *models.ReqContext) response.Respon
query := &models.GetDashboardSnapshotQuery{Key: key}
err := hs.SQLStore.GetDashboardSnapshot(query)
err := hs.DashboardsnapshotsService.GetDashboardSnapshot(c.Req.Context(), query)
if err != nil {
return response.Error(500, "Failed to get dashboard snapshot", err)
}
@@ -224,8 +224,7 @@ func (hs *HTTPServer) DeleteDashboardSnapshotByDeleteKey(c *models.ReqContext) r
}
query := &models.GetDashboardSnapshotQuery{DeleteKey: key}
err := hs.SQLStore.GetDashboardSnapshot(query)
err := hs.DashboardsnapshotsService.GetDashboardSnapshot(c.Req.Context(), query)
if err != nil {
return response.Error(500, "Failed to get dashboard snapshot", err)
}
@@ -239,7 +238,7 @@ func (hs *HTTPServer) DeleteDashboardSnapshotByDeleteKey(c *models.ReqContext) r
cmd := &models.DeleteDashboardSnapshotCommand{DeleteKey: query.Result.DeleteKey}
if err := hs.SQLStore.DeleteDashboardSnapshot(c.Req.Context(), cmd); err != nil {
if err := hs.DashboardsnapshotsService.DeleteDashboardSnapshot(c.Req.Context(), cmd); err != nil {
return response.Error(500, "Failed to delete dashboard snapshot", err)
}
@@ -258,7 +257,7 @@ func (hs *HTTPServer) DeleteDashboardSnapshot(c *models.ReqContext) response.Res
query := &models.GetDashboardSnapshotQuery{Key: key}
err := hs.SQLStore.GetDashboardSnapshot(query)
err := hs.DashboardsnapshotsService.GetDashboardSnapshot(c.Req.Context(), query)
if err != nil {
return response.Error(500, "Failed to get dashboard snapshot", err)
}
@@ -287,7 +286,7 @@ func (hs *HTTPServer) DeleteDashboardSnapshot(c *models.ReqContext) response.Res
cmd := &models.DeleteDashboardSnapshotCommand{DeleteKey: query.Result.DeleteKey}
if err := hs.SQLStore.DeleteDashboardSnapshot(c.Req.Context(), cmd); err != nil {
if err := hs.DashboardsnapshotsService.DeleteDashboardSnapshot(c.Req.Context(), cmd); err != nil {
return response.Error(500, "Failed to delete dashboard snapshot", err)
}
@@ -313,7 +312,7 @@ func (hs *HTTPServer) SearchDashboardSnapshots(c *models.ReqContext) response.Re
SignedInUser: c.SignedInUser,
}
err := hs.SQLStore.SearchDashboardSnapshots(&searchQuery)
err := hs.DashboardsnapshotsService.SearchDashboardSnapshots(c.Req.Context(), &searchQuery)
if err != nil {
return response.Error(500, "Search failed", err)
}
+2 -1
View File
@@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -33,7 +34,7 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) {
editorRole := models.ROLE_EDITOR
sqlmock := mockstore.NewSQLStoreMock()
aclMockResp := []*models.DashboardAclInfoDTO{}
hs := &HTTPServer{SQLStore: sqlmock}
hs := &HTTPServer{DashboardsnapshotsService: &dashboardsnapshots.Service{SQLStore: sqlmock}}
setUpSnapshotTest := func(t *testing.T) *models.DashboardSnapshot {
t.Helper()
+61 -84
View File
@@ -7,7 +7,6 @@ import (
"fmt"
"io/ioutil"
"net/http"
"path/filepath"
"testing"
"github.com/grafana/grafana/pkg/api/dtos"
@@ -15,11 +14,12 @@ import (
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
dboards "github.com/grafana/grafana/pkg/dashboards"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/database"
service "github.com/grafana/grafana/pkg/services/dashboards/manager"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/libraryelements"
"github.com/grafana/grafana/pkg/services/live"
@@ -30,6 +30,7 @@ import (
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
@@ -145,7 +146,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
"/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
setUp()
sc.sqlStore = mockSQLStore
dash := getDashboardShouldReturn200(sc)
dash := getDashboardShouldReturn200(t, sc)
assert.False(t, dash.Meta.CanEdit)
assert.False(t, dash.Meta.CanSave)
@@ -177,7 +178,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
"/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
setUp()
sc.sqlStore = mockSQLStore
dash := getDashboardShouldReturn200(sc)
dash := getDashboardShouldReturn200(t, sc)
assert.True(t, dash.Meta.CanEdit)
assert.True(t, dash.Meta.CanSave)
@@ -212,11 +213,13 @@ func TestDashboardAPIEndpoint(t *testing.T) {
mockSQLStore := mockstore.NewSQLStoreMock()
mockSQLStore.ExpectedDashboard = fakeDash
dashboardStore := database.ProvideDashboardStore(sqlstore.InitTestDB(t))
hs := &HTTPServer{
Cfg: setting.NewCfg(),
Live: newTestLive(t),
LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &mockLibraryElementService{},
dashboardService: service.ProvideDashboardService(dashboardStore),
SQLStore: mockSQLStore,
}
hs.SQLStore = mockSQLStore
@@ -345,7 +348,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
"/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
setUpInner()
sc.sqlStore = mockSQLStore
dash := getDashboardShouldReturn200(sc)
dash := getDashboardShouldReturn200(t, sc)
assert.True(t, dash.Meta.CanEdit)
assert.True(t, dash.Meta.CanSave)
@@ -412,7 +415,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
require.True(t, setting.ViewersCanEdit)
sc.sqlStore = mockSQLStore
dash := getDashboardShouldReturn200(sc)
dash := getDashboardShouldReturn200(t, sc)
assert.True(t, dash.Meta.CanEdit)
assert.False(t, dash.Meta.CanSave)
@@ -445,7 +448,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
setUpInner()
sc.sqlStore = mockSQLStore
dash := getDashboardShouldReturn200(sc)
dash := getDashboardShouldReturn200(t, sc)
assert.True(t, dash.Meta.CanEdit)
assert.True(t, dash.Meta.CanSave)
@@ -493,7 +496,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
setUpInner()
sc.sqlStore = mockSQLStore
dash := getDashboardShouldReturn200(sc)
dash := getDashboardShouldReturn200(t, sc)
assert.False(t, dash.Meta.CanEdit)
assert.False(t, dash.Meta.CanSave)
@@ -535,6 +538,8 @@ func TestDashboardAPIEndpoint(t *testing.T) {
})
t.Run("Post dashboard response tests", func(t *testing.T) {
dashboardStore := &database.FakeDashboardStore{}
defer dashboardStore.AssertExpectations(t)
// This tests that a valid request returns correct response
t.Run("Given a correct request for creating a dashboard", func(t *testing.T) {
const folderID int64 = 3
@@ -562,7 +567,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
},
}
postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", mock, nil, cmd, func(sc *scenarioContext) {
postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", cmd, mock, nil, func(sc *scenarioContext) {
callPostDashboardShouldReturnSuccess(sc)
dto := mock.SavedDashboards[0]
@@ -612,7 +617,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
GetFolderByUIDResult: &models.Folder{Id: 1, Uid: "folderUID", Title: "Folder"},
}
postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", mock, mockFolder, cmd, func(sc *scenarioContext) {
postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", cmd, mock, mockFolder, func(sc *scenarioContext) {
callPostDashboardShouldReturnSuccess(sc)
dto := mock.SavedDashboards[0]
@@ -661,7 +666,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
GetFolderByUIDError: errors.New("Error while searching Folder ID"),
}
postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", mock, mockFolder, cmd, func(sc *scenarioContext) {
postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", cmd, mock, mockFolder, func(sc *scenarioContext) {
callPostDashboard(sc)
assert.Equal(t, 500, sc.resp.Code)
})
@@ -706,7 +711,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
}
postDashboardScenario(t, fmt.Sprintf("Expect '%s' error when calling POST on", tc.SaveError.Error()),
"/api/dashboards", "/api/dashboards", mock, nil, cmd, func(sc *scenarioContext) {
"/api/dashboards", "/api/dashboards", cmd, mock, nil, func(sc *scenarioContext) {
callPostDashboard(sc)
assert.Equal(t, tc.ExpectedStatusCode, sc.resp.Code)
})
@@ -852,14 +857,6 @@ func TestDashboardAPIEndpoint(t *testing.T) {
t.Run("Given provisioned dashboard", func(t *testing.T) {
setUp := func() {
origGetProvisionedData := dashboards.GetProvisionedData
t.Cleanup(func() {
dashboards.GetProvisionedData = origGetProvisionedData
})
dashboards.GetProvisionedData = func(dboards.Store, int64) (*models.DashboardProvisioning, error) {
return &models.DashboardProvisioning{ExternalId: "/tmp/grafana/dashboards/test/dashboard1.json"}, nil
}
bus.AddHandler("test", func(ctx context.Context, query *models.GetDashboardAclInfoListQuery) error {
query.Result = []*models.DashboardAclInfoDTO{
{OrgId: testOrgID, DashboardId: 1, UserId: testUserID, Permission: models.PERMISSION_EDIT},
@@ -867,6 +864,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
return nil
})
}
mockSQLStore := mockstore.NewSQLStoreMock()
dataValue, err := simplejson.NewJson([]byte(`{"id": 1, "editable": true, "style": "dark"}`))
require.NoError(t, err)
@@ -874,37 +872,39 @@ func TestDashboardAPIEndpoint(t *testing.T) {
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/dash", "/api/dashboards/uid/:uid", models.ROLE_EDITOR, func(sc *scenarioContext) {
setUp()
dataValue, err := simplejson.NewJson([]byte(`{"id": 1, "editable": true, "style": "dark"}`))
require.NoError(t, err)
mockSQLStore.ExpectedDashboard = &models.Dashboard{Id: 1, Data: dataValue}
sc.sqlStore = mockSQLStore
mock := provisioning.NewProvisioningServiceMock(context.Background())
mock.GetDashboardProvisionerResolvedPathFunc = func(name string) string {
fakeProvisioningService := provisioning.NewProvisioningServiceMock(context.Background())
fakeProvisioningService.GetDashboardProvisionerResolvedPathFunc = func(name string) string {
return "/tmp/grafana/dashboards"
}
dash := getDashboardShouldReturn200WithConfig(sc, mock)
dashboardStore := &database.FakeDashboardStore{}
defer dashboardStore.AssertExpectations(t)
assert.Equal(t, filepath.Join("test", "dashboard1.json"), dash.Meta.ProvisionedExternalId)
dashboardStore.On("GetProvisionedDataByDashboardID", mock.Anything).Return(&models.DashboardProvisioning{ExternalId: "/dashboard1.json"}, nil).Once()
dash := getDashboardShouldReturn200WithConfig(t, sc, fakeProvisioningService, dashboardStore)
assert.Equal(t, "../../../dashboard1.json", dash.Meta.ProvisionedExternalId, mockSQLStore)
}, mockSQLStore)
loggedInUserScenarioWithRole(t, "When allowUiUpdates is true and calling GET on", "GET", "/api/dashboards/uid/dash", "/api/dashboards/uid/:uid", models.ROLE_EDITOR, func(sc *scenarioContext) {
setUp()
mock := provisioning.NewProvisioningServiceMock(context.Background())
mock.GetDashboardProvisionerResolvedPathFunc = func(name string) string {
fakeProvisioningService := provisioning.NewProvisioningServiceMock(context.Background())
fakeProvisioningService.GetDashboardProvisionerResolvedPathFunc = func(name string) string {
return "/tmp/grafana/dashboards"
}
mock.GetAllowUIUpdatesFromConfigFunc = func(name string) bool {
fakeProvisioningService.GetAllowUIUpdatesFromConfigFunc = func(name string) bool {
return true
}
hs := &HTTPServer{
Cfg: setting.NewCfg(),
ProvisioningService: mock,
LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &mockLibraryElementService{},
SQLStore: mockSQLStore,
Cfg: setting.NewCfg(),
ProvisioningService: fakeProvisioningService,
LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &mockLibraryElementService{},
dashboardProvisioningService: mockDashboardProvisioningService{},
SQLStore: mockSQLStore,
}
hs.callGetDashboard(sc)
@@ -919,21 +919,28 @@ func TestDashboardAPIEndpoint(t *testing.T) {
})
}
func getDashboardShouldReturn200WithConfig(sc *scenarioContext, provisioningService provisioning.ProvisioningService) dtos.
DashboardFullWithMeta {
func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, provisioningService provisioning.ProvisioningService, dashboardStore dashboards.Store) dtos.DashboardFullWithMeta {
t.Helper()
if provisioningService == nil {
provisioningService = provisioning.NewProvisioningServiceMock(context.Background())
}
if dashboardStore == nil {
sql := sqlstore.InitTestDB(t)
dashboardStore = database.ProvideDashboardStore(sql)
}
libraryPanelsService := mockLibraryPanelService{}
libraryElementsService := mockLibraryElementService{}
hs := &HTTPServer{
Cfg: setting.NewCfg(),
LibraryPanelService: &libraryPanelsService,
LibraryElementService: &libraryElementsService,
ProvisioningService: provisioningService,
SQLStore: sc.sqlStore,
Cfg: setting.NewCfg(),
LibraryPanelService: &libraryPanelsService,
LibraryElementService: &libraryElementsService,
ProvisioningService: provisioningService,
dashboardProvisioningService: service.ProvideDashboardService(dashboardStore),
SQLStore: sc.sqlStore,
}
hs.callGetDashboard(sc)
@@ -947,8 +954,8 @@ func getDashboardShouldReturn200WithConfig(sc *scenarioContext, provisioningServ
return dash
}
func getDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta {
return getDashboardShouldReturn200WithConfig(sc, nil)
func getDashboardShouldReturn200(t *testing.T, sc *scenarioContext) dtos.DashboardFullWithMeta {
return getDashboardShouldReturn200WithConfig(t, sc, nil, nil)
}
func (hs *HTTPServer) callGetDashboard(sc *scenarioContext) {
@@ -976,17 +983,13 @@ func (hs *HTTPServer) callGetDashboardVersions(sc *scenarioContext) {
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
}
func (hs *HTTPServer) callDeleteDashboardByUID(t *testing.T, sc *scenarioContext, mockDashboard *dashboards.FakeDashboardService) {
func (hs *HTTPServer) callDeleteDashboardByUID(t *testing.T,
sc *scenarioContext, mockDashboard *dashboards.FakeDashboardService) {
bus.AddHandler("test", func(ctx context.Context, cmd *models.DeleteDashboardCommand) error {
return nil
})
origNewDashboardService := dashboards.NewService
t.Cleanup(func() {
dashboards.NewService = origNewDashboardService
})
dashboards.MockDashboardService(mockDashboard)
hs.dashboardService = mockDashboard
sc.handlerFunc = hs.DeleteDashboardByUID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
}
@@ -1005,9 +1008,7 @@ func callPostDashboardShouldReturnSuccess(sc *scenarioContext) {
assert.Equal(sc.t, 200, sc.resp.Code)
}
func postDashboardScenario(t *testing.T, desc string, url string, routePattern string,
mock *dashboards.FakeDashboardService, mockFolder *fakeFolderService, cmd models.SaveDashboardCommand,
fn scenarioFunc) {
func postDashboardScenario(t *testing.T, desc string, url string, routePattern string, cmd models.SaveDashboardCommand, dashboardService dashboards.DashboardService, folderService dashboards.FolderService, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
@@ -1023,6 +1024,8 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s
pluginStore: &fakePluginStore{},
LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &mockLibraryElementService{},
dashboardService: dashboardService,
folderService: folderService,
}
sc := setupScenarioContext(t, url)
@@ -1035,20 +1038,6 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s
return hs.PostDashboard(c)
})
origNewDashboardService := dashboards.NewService
origProvisioningService := dashboards.NewProvisioningService
origNewFolderService := dashboards.NewFolderService
t.Cleanup(func() {
dashboards.NewService = origNewDashboardService
dashboards.NewProvisioningService = origProvisioningService
dashboards.NewFolderService = origNewFolderService
})
dashboards.MockDashboardService(mock)
dashboards.NewProvisioningService = func(dboards.Store) dashboards.DashboardProvisioningService {
return mockDashboardProvisioningService{}
}
mockFolderService(mockFolder)
sc.m.Post(routePattern, sc.defaultHandler)
fn(sc)
@@ -1091,9 +1080,7 @@ func postDiffScenario(t *testing.T, desc string, url string, routePattern string
})
}
func restoreDashboardVersionScenario(t *testing.T, desc string, url string, routePattern string,
mock *dashboards.FakeDashboardService, cmd dtos.RestoreDashboardVersionCommand, fn scenarioFunc,
sqlStore sqlstore.Store) {
func restoreDashboardVersionScenario(t *testing.T, desc string, url string, routePattern string, mock *dashboards.FakeDashboardService, cmd dtos.RestoreDashboardVersionCommand, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
defer bus.ClearBusHandlers()
@@ -1107,6 +1094,7 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout
QuotaService: &quota.QuotaService{Cfg: cfg},
LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &mockLibraryElementService{},
dashboardService: mock,
SQLStore: sqlStore,
}
@@ -1125,17 +1113,6 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout
return hs.RestoreDashboardVersion(c)
})
origProvisioningService := dashboards.NewProvisioningService
origNewDashboardService := dashboards.NewService
t.Cleanup(func() {
dashboards.NewService = origNewDashboardService
dashboards.NewProvisioningService = origProvisioningService
})
dashboards.NewProvisioningService = func(dboards.Store) dashboards.DashboardProvisioningService {
return mockDashboardProvisioningService{}
}
dashboards.MockDashboardService(mock)
sc.m.Post(routePattern, sc.defaultHandler)
fn(sc)
+3 -28
View File
@@ -17,7 +17,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins/adapters"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
@@ -71,23 +70,6 @@ func (hs *HTTPServer) GetDataSources(c *models.ReqContext) response.Response {
return response.JSON(200, &result)
}
func (hs *HTTPServer) getDataSourceAccessControlMetadata(c *models.ReqContext, dsID int64) (accesscontrol.Metadata, error) {
if hs.AccessControl.IsDisabled() || !c.QueryBool("accesscontrol") {
return nil, nil
}
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser,
accesscontrol.Options{ReloadCache: false})
if err != nil || len(userPermissions) == 0 {
return nil, err
}
key := fmt.Sprintf("%d", dsID)
dsIDs := map[string]bool{key: true}
return accesscontrol.GetResourcesMetadata(c.Req.Context(), userPermissions, "datasources", dsIDs)[key], nil
}
// GET /api/datasources/:id
func (hs *HTTPServer) GetDataSourceById(c *models.ReqContext) response.Response {
id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64)
@@ -115,12 +97,9 @@ func (hs *HTTPServer) GetDataSourceById(c *models.ReqContext) response.Response
}
dto := convertModelToDtos(filtered[0])
// Add accesscontrol metadata
metadata, err := hs.getDataSourceAccessControlMetadata(c, dto.Id)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to query metadata", err)
}
dto.AccessControl = metadata
dto.AccessControl = hs.getAccessControlMetadata(c, "datasources", dto.Id)
return response.JSON(200, &dto)
}
@@ -179,11 +158,7 @@ func (hs *HTTPServer) GetDataSourceByUID(c *models.ReqContext) response.Response
dto := convertModelToDtos(filtered[0])
// Add accesscontrol metadata
metadata, err := hs.getDataSourceAccessControlMetadata(c, dto.Id)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to query metadata", err)
}
dto.AccessControl = metadata
dto.AccessControl = hs.getAccessControlMetadata(c, "datasources", dto.Id)
return response.JSON(200, &dto)
}
+1
View File
@@ -71,6 +71,7 @@ type NavLink struct {
HideFromTabs bool `json:"hideFromTabs,omitempty"`
Children []*NavLink `json:"children,omitempty"`
HighlightText string `json:"highlightText,omitempty"`
HighlightID string `json:"highlightId,omitempty"`
}
// NavIDCfg is the id for org configuration navigation node
+7 -16
View File
@@ -11,7 +11,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/dashboards"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/libraryelements"
"github.com/grafana/grafana/pkg/util"
@@ -19,8 +18,7 @@ import (
)
func (hs *HTTPServer) GetFolders(c *models.ReqContext) response.Response {
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore)
folders, err := s.GetFolders(c.Req.Context(), c.QueryInt64("limit"), c.QueryInt64("page"))
folders, err := hs.folderService.GetFolders(c.Req.Context(), c.SignedInUser, c.OrgId, c.QueryInt64("limit"), c.QueryInt64("page"))
if err != nil {
return apierrors.ToFolderErrorResponse(err)
@@ -40,8 +38,7 @@ func (hs *HTTPServer) GetFolders(c *models.ReqContext) response.Response {
}
func (hs *HTTPServer) GetFolderByUID(c *models.ReqContext) response.Response {
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore)
folder, err := s.GetFolderByUID(c.Req.Context(), web.Params(c.Req)[":uid"])
folder, err := hs.folderService.GetFolderByUID(c.Req.Context(), c.SignedInUser, c.OrgId, web.Params(c.Req)[":uid"])
if err != nil {
return apierrors.ToFolderErrorResponse(err)
}
@@ -51,14 +48,11 @@ func (hs *HTTPServer) GetFolderByUID(c *models.ReqContext) response.Response {
}
func (hs *HTTPServer) GetFolderByID(c *models.ReqContext) response.Response {
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore)
id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "id is invalid", err)
}
folder, err := s.GetFolderByID(c.Req.Context(), id)
folder, err := hs.folderService.GetFolderByID(c.Req.Context(), c.SignedInUser, c.OrgId, id)
if err != nil {
return apierrors.ToFolderErrorResponse(err)
}
@@ -72,14 +66,13 @@ func (hs *HTTPServer) CreateFolder(c *models.ReqContext) response.Response {
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore)
folder, err := s.CreateFolder(c.Req.Context(), cmd.Title, cmd.Uid)
folder, err := hs.folderService.CreateFolder(c.Req.Context(), c.SignedInUser, c.OrgId, cmd.Title, cmd.Uid)
if err != nil {
return apierrors.ToFolderErrorResponse(err)
}
if hs.Cfg.EditorsCanAdmin {
if err := s.MakeUserAdmin(c.Req.Context(), c.OrgId, c.SignedInUser.UserId, folder.Id, true); err != nil {
if err := hs.folderService.MakeUserAdmin(c.Req.Context(), c.OrgId, c.SignedInUser.UserId, folder.Id, true); err != nil {
hs.log.Error("Could not make user admin", "folder", folder.Title, "user",
c.SignedInUser.UserId, "error", err)
}
@@ -94,8 +87,7 @@ func (hs *HTTPServer) UpdateFolder(c *models.ReqContext) response.Response {
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore)
err := s.UpdateFolder(c.Req.Context(), web.Params(c.Req)[":uid"], &cmd)
err := hs.folderService.UpdateFolder(c.Req.Context(), c.SignedInUser, c.OrgId, web.Params(c.Req)[":uid"], &cmd)
if err != nil {
return apierrors.ToFolderErrorResponse(err)
}
@@ -105,7 +97,6 @@ func (hs *HTTPServer) UpdateFolder(c *models.ReqContext) response.Response {
}
func (hs *HTTPServer) DeleteFolder(c *models.ReqContext) response.Response { // temporarily adding this function to HTTPServer, will be removed from HTTPServer when librarypanels featuretoggle is removed
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore)
err := hs.LibraryElementService.DeleteLibraryElementsInFolder(c.Req.Context(), c.SignedInUser, web.Params(c.Req)[":uid"])
if err != nil {
if errors.Is(err, libraryelements.ErrFolderHasConnectedLibraryElements) {
@@ -114,7 +105,7 @@ func (hs *HTTPServer) DeleteFolder(c *models.ReqContext) response.Response { //
return apierrors.ToFolderErrorResponse(err)
}
f, err := s.DeleteFolder(c.Req.Context(), web.Params(c.Req)[":uid"], c.QueryBool("forceDeleteRules"))
f, err := hs.folderService.DeleteFolder(c.Req.Context(), c.SignedInUser, c.OrgId, web.Params(c.Req)[":uid"], c.QueryBool("forceDeleteRules"))
if err != nil {
return apierrors.ToFolderErrorResponse(err)
}
+3 -6
View File
@@ -9,15 +9,13 @@ 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/dashboards"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
func (hs *HTTPServer) GetFolderPermissionList(c *models.ReqContext) response.Response {
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore)
folder, err := s.GetFolderByUID(c.Req.Context(), web.Params(c.Req)[":uid"])
folder, err := hs.folderService.GetFolderByUID(c.Req.Context(), c.SignedInUser, c.OrgId, web.Params(c.Req)[":uid"])
if err != nil {
return apierrors.ToFolderErrorResponse(err)
@@ -68,8 +66,7 @@ func (hs *HTTPServer) UpdateFolderPermissions(c *models.ReqContext) response.Res
return response.Error(400, err.Error(), err)
}
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore)
folder, err := s.GetFolderByUID(c.Req.Context(), web.Params(c.Req)[":uid"])
folder, err := hs.folderService.GetFolderByUID(c.Req.Context(), c.SignedInUser, c.OrgId, web.Params(c.Req)[":uid"])
if err != nil {
return apierrors.ToFolderErrorResponse(err)
}
@@ -117,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 err := updateDashboardACL(c.Req.Context(), hs.SQLStore, folder.Id, items); err != nil {
if err := hs.dashboardService.UpdateDashboardACL(c.Req.Context(), folder.Id, items); err != nil {
if errors.Is(err, models.ErrDashboardAclInfoMissing) {
err = models.ErrFolderAclInfoMissing
}
+28 -87
View File
@@ -1,7 +1,6 @@
package api
import (
"context"
"encoding/json"
"fmt"
"testing"
@@ -13,28 +12,28 @@ import (
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/bus"
dashboardifaces "github.com/grafana/grafana/pkg/dashboards"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/database"
service "github.com/grafana/grafana/pkg/services/dashboards/manager"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/mock"
)
func TestFolderPermissionAPIEndpoint(t *testing.T) {
settings := setting.NewCfg()
hs := &HTTPServer{Cfg: settings}
folderService := &dashboards.FakeFolderService{}
defer folderService.AssertExpectations(t)
dashboardStore := &database.FakeDashboardStore{}
defer dashboardStore.AssertExpectations(t)
hs := &HTTPServer{Cfg: settings, folderService: folderService, dashboardService: service.ProvideDashboardService(dashboardStore)}
t.Run("Given folder not exists", func(t *testing.T) {
mock := &fakeFolderService{
GetFolderByUIDError: models.ErrFolderNotFound,
}
origNewFolderService := dashboards.NewFolderService
t.Cleanup(func() {
dashboards.NewFolderService = origNewFolderService
})
mockFolderService(mock)
folderService.On("GetFolderByUID", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, models.ErrFolderNotFound).Twice()
mockSQLStore := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", models.ROLE_EDITOR, func(sc *scenarioContext) {
callGetFolderPermissions(sc, hs)
@@ -61,24 +60,14 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
t.Run("Given user has no admin permissions", func(t *testing.T) {
origNewGuardian := guardian.New
origNewFolderService := dashboards.NewFolderService
t.Cleanup(func() {
guardian.New = origNewGuardian
dashboards.NewFolderService = origNewFolderService
})
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanAdminValue: false})
mock := &fakeFolderService{
GetFolderByUIDResult: &models.Folder{
Id: 1,
Uid: "uid",
Title: "Folder",
},
}
mockFolderService(mock)
folderService.On("GetFolderByUID", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, models.ErrFolderAccessDenied).Twice()
mockSQLStore := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", models.ROLE_EDITOR, func(sc *scenarioContext) {
callGetFolderPermissions(sc, hs)
assert.Equal(t, 403, sc.resp.Code)
@@ -104,10 +93,8 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
t.Run("Given user has admin permissions and permissions to update", func(t *testing.T) {
origNewGuardian := guardian.New
origNewFolderService := dashboards.NewFolderService
t.Cleanup(func() {
guardian.New = origNewGuardian
dashboards.NewFolderService = origNewFolderService
})
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
@@ -122,16 +109,11 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
},
})
mock := &fakeFolderService{
GetFolderByUIDResult: &models.Folder{
Id: 1,
Uid: "uid",
Title: "Folder",
},
}
mockFolderService(mock)
folderResponse := &models.Folder{Id: 1, Uid: "uid", Title: "Folder"}
folderService.On("GetFolderByUID", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(folderResponse, nil).Twice()
dashboardStore.On("UpdateDashboardACL", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
mockSQLStore := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", models.ROLE_ADMIN, func(sc *scenarioContext) {
callGetFolderPermissions(sc, hs)
assert.Equal(t, 200, sc.resp.Code)
@@ -175,10 +157,8 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
t.Run("When trying to update permissions with duplicate permissions", func(t *testing.T) {
origNewGuardian := guardian.New
origNewFolderService := dashboards.NewFolderService
t.Cleanup(func() {
guardian.New = origNewGuardian
dashboards.NewFolderService = origNewFolderService
})
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
@@ -187,15 +167,8 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
CheckPermissionBeforeUpdateError: guardian.ErrGuardianPermissionExists,
})
mock := &fakeFolderService{
GetFolderByUIDResult: &models.Folder{
Id: 1,
Uid: "uid",
Title: "Folder",
},
}
mockFolderService(mock)
folderResponse := &models.Folder{Id: 1, Uid: "uid", Title: "Folder"}
folderService.On("GetFolderByUID", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(folderResponse, nil).Once()
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
@@ -249,10 +222,8 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
t.Run("When trying to override inherited permissions with lower precedence", func(t *testing.T) {
origNewGuardian := guardian.New
origNewFolderService := dashboards.NewFolderService
t.Cleanup(func() {
guardian.New = origNewGuardian
dashboards.NewFolderService = origNewFolderService
})
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
@@ -261,15 +232,8 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
CheckPermissionBeforeUpdateError: guardian.ErrGuardianOverride},
)
mock := &fakeFolderService{
GetFolderByUIDResult: &models.Folder{
Id: 1,
Uid: "uid",
Title: "Folder",
},
}
mockFolderService(mock)
folderResponse := &models.Folder{Id: 1, Uid: "uid", Title: "Folder"}
folderService.On("GetFolderByUID", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(folderResponse, nil).Once()
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
@@ -291,14 +255,12 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
t.Run("Getting and updating folder permissions with hidden users", func(t *testing.T) {
origNewGuardian := guardian.New
origNewFolderService := dashboards.NewFolderService
settings.HiddenUsers = map[string]struct{}{
"hiddenUser": {},
testUserLogin: {},
}
t.Cleanup(func() {
guardian.New = origNewGuardian
dashboards.NewFolderService = origNewFolderService
settings.HiddenUsers = make(map[string]struct{})
})
@@ -315,15 +277,13 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
},
})
mock := &fakeFolderService{
GetFolderByUIDResult: &models.Folder{
Id: 1,
Uid: "uid",
Title: "Folder",
},
}
var gotItems []*models.DashboardAcl
mockFolderService(mock)
folderResponse := &models.Folder{Id: 1, Uid: "uid", Title: "Folder"}
folderService.On("GetFolderByUID", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(folderResponse, nil).Twice()
dashboardStore.On("UpdateDashboardACL", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
gotItems = args.Get(2).([]*models.DashboardAcl)
}).Return(nil).Once()
var resp []*models.DashboardAclInfoDTO
mockSQLStore := mockstore.NewSQLStoreMock()
@@ -360,16 +320,6 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
routePattern: "/api/folders/:uid/permissions",
cmd: cmd,
fn: func(sc *scenarioContext) {
origUpdateDashboardACL := updateDashboardACL
t.Cleanup(func() {
updateDashboardACL = origUpdateDashboardACL
})
var gotItems []*models.DashboardAcl
updateDashboardACL = func(_ context.Context, _ dashboardifaces.Store, _ int64, items []*models.DashboardAcl) error {
gotItems = items
return nil
}
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
assert.Len(t, gotItems, 4)
@@ -385,21 +335,12 @@ func callGetFolderPermissions(sc *scenarioContext, hs *HTTPServer) {
func callUpdateFolderPermissions(t *testing.T, sc *scenarioContext) {
t.Helper()
origUpdateDashboardACL := updateDashboardACL
t.Cleanup(func() {
updateDashboardACL = origUpdateDashboardACL
})
updateDashboardACL = func(_ context.Context, _ dashboardifaces.Store, dashID int64, items []*models.DashboardAcl) error {
return nil
}
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
}
func updateFolderPermissionScenario(t *testing.T, ctx updatePermissionContext, hs *HTTPServer) {
t.Run(fmt.Sprintf("%s %s", ctx.desc, ctx.url), func(t *testing.T) {
defer bus.ClearBusHandlers()
t.Cleanup(bus.ClearBusHandlers)
sc := setupScenarioContext(t, ctx.url)
+29 -49
View File
@@ -10,26 +10,28 @@ import (
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/bus"
dboards "github.com/grafana/grafana/pkg/dashboards"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func TestFoldersAPIEndpoint(t *testing.T) {
folderService := &dashboards.FakeFolderService{}
defer folderService.AssertExpectations(t)
t.Run("Given a correct request for creating a folder", func(t *testing.T) {
cmd := models.CreateFolderCommand{
Uid: "uid",
Title: "Folder",
}
mock := &fakeFolderService{
CreateFolderResult: &models.Folder{Id: 1, Uid: "uid", Title: "Folder"},
}
folderResult := &models.Folder{Id: 1, Uid: "uid", Title: "Folder"}
folderService.On("CreateFolder", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(folderResult, nil).Once()
createFolderScenario(t, "When calling POST on", "/api/folders", "/api/folders", mock, cmd,
createFolderScenario(t, "When calling POST on", "/api/folders", "/api/folders", folderService, cmd,
func(sc *scenarioContext) {
callCreateFolder(sc)
@@ -64,12 +66,10 @@ func TestFoldersAPIEndpoint(t *testing.T) {
}
for _, tc := range testCases {
mock := &fakeFolderService{
CreateFolderError: tc.Error,
}
folderService.On("CreateFolder", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, tc.Error).Once()
createFolderScenario(t, fmt.Sprintf("Expect '%s' error when calling POST on", tc.Error.Error()),
"/api/folders", "/api/folders", mock, cmd, func(sc *scenarioContext) {
"/api/folders", "/api/folders", folderService, cmd, func(sc *scenarioContext) {
callCreateFolder(sc)
assert.Equalf(t, tc.ExpectedStatusCode, sc.resp.Code, "Wrong status code for error %s", tc.Error)
})
@@ -81,11 +81,12 @@ func TestFoldersAPIEndpoint(t *testing.T) {
Title: "Folder upd",
}
mock := &fakeFolderService{
UpdateFolderResult: &models.Folder{Id: 1, Uid: "uid", Title: "Folder upd"},
}
folderService.On("UpdateFolder", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
cmd := args.Get(4).(*models.UpdateFolderCommand)
cmd.Result = &models.Folder{Id: 1, Uid: "uid", Title: "Folder upd"}
}).Return(nil).Once()
updateFolderScenario(t, "When calling PUT on", "/api/folders/uid", "/api/folders/:uid", mock, cmd,
updateFolderScenario(t, "When calling PUT on", "/api/folders/uid", "/api/folders/:uid", folderService, cmd,
func(sc *scenarioContext) {
callUpdateFolder(sc)
@@ -119,12 +120,9 @@ func TestFoldersAPIEndpoint(t *testing.T) {
}
for _, tc := range testCases {
mock := &fakeFolderService{
UpdateFolderError: tc.Error,
}
folderService.On("UpdateFolder", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.Error).Once()
updateFolderScenario(t, fmt.Sprintf("Expect '%s' error when calling PUT on", tc.Error.Error()),
"/api/folders/uid", "/api/folders/:uid", mock, cmd, func(sc *scenarioContext) {
"/api/folders/uid", "/api/folders/:uid", folderService, cmd, func(sc *scenarioContext) {
callUpdateFolder(sc)
assert.Equalf(t, tc.ExpectedStatusCode, sc.resp.Code, "Wrong status code for %s", tc.Error)
})
@@ -136,14 +134,15 @@ func callCreateFolder(sc *scenarioContext) {
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
}
func createFolderScenario(t *testing.T, desc string, url string, routePattern string, mock *fakeFolderService,
func createFolderScenario(t *testing.T, desc string, url string, routePattern string, folderService dashboards.FolderService,
cmd models.CreateFolderCommand, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
hs := HTTPServer{
Bus: bus.GetBus(),
Cfg: setting.NewCfg(),
Bus: bus.GetBus(),
Cfg: setting.NewCfg(),
folderService: folderService,
}
sc := setupScenarioContext(t, url)
@@ -156,15 +155,8 @@ func createFolderScenario(t *testing.T, desc string, url string, routePattern st
return hs.CreateFolder(c)
})
origNewFolderService := dashboards.NewFolderService
mockFolderService(mock)
sc.m.Post(routePattern, sc.defaultHandler)
defer func() {
dashboards.NewFolderService = origNewFolderService
}()
fn(sc)
})
}
@@ -173,13 +165,14 @@ func callUpdateFolder(sc *scenarioContext) {
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
}
func updateFolderScenario(t *testing.T, desc string, url string, routePattern string, mock *fakeFolderService,
func updateFolderScenario(t *testing.T, desc string, url string, routePattern string, folderService dashboards.FolderService,
cmd models.UpdateFolderCommand, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
defer bus.ClearBusHandlers()
hs := HTTPServer{
Cfg: setting.NewCfg(),
Cfg: setting.NewCfg(),
folderService: folderService,
}
sc := setupScenarioContext(t, url)
@@ -192,12 +185,6 @@ func updateFolderScenario(t *testing.T, desc string, url string, routePattern st
return hs.UpdateFolder(c)
})
origNewFolderService := dashboards.NewFolderService
t.Cleanup(func() {
dashboards.NewFolderService = origNewFolderService
})
mockFolderService(mock)
sc.m.Put(routePattern, sc.defaultHandler)
fn(sc)
@@ -222,35 +209,28 @@ type fakeFolderService struct {
DeletedFolderUids []string
}
func (s *fakeFolderService) GetFolders(ctx context.Context, limit int64, page int64) ([]*models.Folder, error) {
func (s *fakeFolderService) GetFolders(ctx context.Context, user *models.SignedInUser, orgID int64, limit int64, page int64) ([]*models.Folder, error) {
return s.GetFoldersResult, s.GetFoldersError
}
func (s *fakeFolderService) GetFolderByID(ctx context.Context, id int64) (*models.Folder, error) {
func (s *fakeFolderService) GetFolderByID(ctx context.Context, user *models.SignedInUser, id int64, orgID int64) (*models.Folder, error) {
return s.GetFolderByIDResult, s.GetFolderByIDError
}
func (s *fakeFolderService) GetFolderByUID(ctx context.Context, uid string) (*models.Folder, error) {
func (s *fakeFolderService) GetFolderByUID(ctx context.Context, user *models.SignedInUser, orgID int64, uid string) (*models.Folder, error) {
return s.GetFolderByUIDResult, s.GetFolderByUIDError
}
func (s *fakeFolderService) CreateFolder(ctx context.Context, title, uid string) (*models.Folder, error) {
func (s *fakeFolderService) CreateFolder(ctx context.Context, user *models.SignedInUser, orgID int64, title, uid string) (*models.Folder, error) {
return s.CreateFolderResult, s.CreateFolderError
}
func (s *fakeFolderService) UpdateFolder(ctx context.Context, existingUID string, cmd *models.UpdateFolderCommand) error {
func (s *fakeFolderService) UpdateFolder(ctx context.Context, user *models.SignedInUser, orgID int64, existingUid string, cmd *models.UpdateFolderCommand) error {
cmd.Result = s.UpdateFolderResult
return s.UpdateFolderError
}
func (s *fakeFolderService) DeleteFolder(ctx context.Context, uid string, forceDeleteRules bool) (*models.Folder, error) {
func (s *fakeFolderService) DeleteFolder(ctx context.Context, user *models.SignedInUser, orgID int64, uid string, forceDeleteRules bool) (*models.Folder, error) {
s.DeletedFolderUids = append(s.DeletedFolderUids, uid)
return s.DeleteFolderResult, s.DeleteFolderError
}
func mockFolderService(mock *fakeFolderService) {
dashboards.NewFolderService = func(orgId int64, user *models.SignedInUser,
dashboardStore dboards.Store) dashboards.FolderService {
return mock
}
}
+5
View File
@@ -229,6 +229,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
"externalUserMngLinkUrl": setting.ExternalUserMngLinkUrl,
"externalUserMngLinkName": setting.ExternalUserMngLinkName,
"viewersCanEdit": setting.ViewersCanEdit,
"angularSupportEnabled": hs.Cfg.AngularSupportEnabled,
"editorsCanAdmin": hs.Cfg.EditorsCanAdmin,
"disableSanitizeHtml": hs.Cfg.DisableSanitizeHtml,
"pluginsToPreload": pluginsToPreload,
@@ -274,6 +275,10 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
"unifiedAlertingEnabled": hs.Cfg.UnifiedAlerting.Enabled,
}
if hs.ThumbService != nil {
jsonObj["dashboardPreviews"] = hs.ThumbService.GetDashboardPreviewsSetupSettings(c)
}
if hs.Cfg.GeomapDefaultBaseLayerConfig != nil {
jsonObj["geomapDefaultBaseLayerConfig"] = hs.Cfg.GeomapDefaultBaseLayerConfig
}
+20 -6
View File
@@ -28,11 +28,11 @@ import (
"github.com/grafana/grafana/pkg/plugins/plugincontext"
"github.com/grafana/grafana/pkg/services/accesscontrol"
acmiddleware "github.com/grafana/grafana/pkg/services/accesscontrol/middleware"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourceservices"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/cleanup"
"github.com/grafana/grafana/pkg/services/contexthandler"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
"github.com/grafana/grafana/pkg/services/datasourceproxy"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/encryption"
@@ -128,9 +128,14 @@ type HTTPServer struct {
queryDataService *query.Service
serviceAccountsService serviceaccounts.Service
authInfoService login.AuthInfoService
TeamPermissionsService *resourcepermissions.Service
teamPermissionsService accesscontrol.PermissionsService
NotificationService *notifications.NotificationService
dashboardService dashboards.DashboardService
dashboardProvisioningService dashboards.DashboardProvisioningService
folderService dashboards.FolderService
DatasourcePermissionsService DatasourcePermissionsService
AlertNotificationService *alerting.AlertNotificationService
DashboardsnapshotsService *dashboardsnapshots.Service
}
type ServerOptions struct {
@@ -157,8 +162,12 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
pluginsUpdateChecker *updatechecker.PluginsService, searchUsersService searchusers.Service,
dataSourcesService datasources.DataSourceService, secretsService secrets.Service, queryDataService *query.Service,
ldapGroups ldap.Groups, teamGuardian teamguardian.TeamGuardian, serviceaccountsService serviceaccounts.Service,
authInfoService login.AuthInfoService, resourcePermissionServices *resourceservices.ResourceServices,
notificationService *notifications.NotificationService, datasourcePermissionsService DatasourcePermissionsService) (*HTTPServer, error) {
authInfoService login.AuthInfoService, permissionsServices accesscontrol.PermissionsServices,
notificationService *notifications.NotificationService, dashboardService dashboards.DashboardService,
dashboardProvisioningService dashboards.DashboardProvisioningService, folderService dashboards.FolderService,
datasourcePermissionsService DatasourcePermissionsService, alertNotificationService *alerting.AlertNotificationService,
dashboardsnapshotsService *dashboardsnapshots.Service,
) (*HTTPServer, error) {
web.Env = cfg.Env
m := web.New()
@@ -217,9 +226,14 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
queryDataService: queryDataService,
serviceAccountsService: serviceaccountsService,
authInfoService: authInfoService,
TeamPermissionsService: resourcePermissionServices.GetTeamService(),
NotificationService: notificationService,
dashboardService: dashboardService,
dashboardProvisioningService: dashboardProvisioningService,
folderService: folderService,
DatasourcePermissionsService: datasourcePermissionsService,
teamPermissionsService: permissionsServices.GetTeamService(),
AlertNotificationService: alertNotificationService,
DashboardsnapshotsService: dashboardsnapshotsService,
}
if hs.Listener != nil {
hs.log.Debug("Using provided listener")
+4 -1
View File
@@ -92,7 +92,10 @@ func (hs *HTTPServer) LoginView(c *models.ReqContext) {
enabledOAuths := make(map[string]interface{})
providers := hs.SocialService.GetOAuthInfoProviders()
for key, oauth := range providers {
enabledOAuths[key] = map[string]string{"name": oauth.Name}
enabledOAuths[key] = map[string]string{
"name": oauth.Name,
"icon": oauth.Icon,
}
}
viewData.Settings["oauth"] = enabledOAuths
+5 -25
View File
@@ -10,7 +10,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/accesscontrol"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
@@ -113,19 +112,6 @@ func (hs *HTTPServer) GetOrgUsersForCurrentOrgLookup(c *models.ReqContext) respo
return response.JSON(200, result)
}
func (hs *HTTPServer) getUserAccessControlMetadata(c *models.ReqContext, resourceIDs map[string]bool) (map[string]accesscontrol.Metadata, error) {
if hs.AccessControl == nil || hs.AccessControl.IsDisabled() || !c.QueryBool("accesscontrol") {
return nil, nil
}
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser, accesscontrol.Options{ReloadCache: false})
if err != nil || len(userPermissions) == 0 {
return nil, err
}
return accesscontrol.GetResourcesMetadata(c.Req.Context(), userPermissions, "users", resourceIDs), nil
}
// GET /api/orgs/:orgId/users
func (hs *HTTPServer) GetOrgUsers(c *models.ReqContext) response.Response {
orgId, err := strconv.ParseInt(web.Params(c.Req)[":orgId"], 10, 64)
@@ -164,17 +150,11 @@ func (hs *HTTPServer) getOrgUsersHelper(c *models.ReqContext, query *models.GetO
filteredUsers = append(filteredUsers, user)
}
accessControlMetadata, errAC := hs.getUserAccessControlMetadata(c, userIDs)
if errAC != nil {
hs.log.Error("Failed to get access control metadata", "error", errAC)
return filteredUsers, nil
} else if accessControlMetadata == nil {
return filteredUsers, nil
}
for i := range filteredUsers {
filteredUsers[i].AccessControl = accessControlMetadata[fmt.Sprint(filteredUsers[i].UserId)]
accessControlMetadata := hs.getMultiAccessControlMetadata(c, "users", userIDs)
if len(accessControlMetadata) > 0 {
for i := range filteredUsers {
filteredUsers[i].AccessControl = accessControlMetadata[fmt.Sprint(filteredUsers[i].UserId)]
}
}
return filteredUsers, nil
+2 -2
View File
@@ -150,7 +150,7 @@ func TestOrgUsersAPIEndpoint_LegacyAccessControl_FolderAdmin(t *testing.T) {
"tags": "prod",
}),
}
folder, err := sc.db.SaveDashboard(cmd)
folder, err := sc.dashboardsStore.SaveDashboard(cmd)
require.NoError(t, err)
require.NotNil(t, folder)
@@ -165,7 +165,7 @@ func TestOrgUsersAPIEndpoint_LegacyAccessControl_FolderAdmin(t *testing.T) {
Updated: time.Now(),
},
}
err = sc.db.UpdateDashboardACL(context.Background(), folder.Id, acls)
err = sc.dashboardsStore.UpdateDashboardACL(context.Background(), folder.Id, acls)
require.NoError(t, err)
response := callAPI(sc.server, http.MethodGet, "/api/org/users/lookup", nil, t)
+29 -28
View File
@@ -21,6 +21,7 @@ import (
"github.com/grafana/grafana/pkg/plugins"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
datasourceservice "github.com/grafana/grafana/pkg/services/datasources/service"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/oauthtoken"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
@@ -129,7 +130,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("When matching route path", func(t *testing.T) {
ctx, req := setUp()
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/v4/some/method", cfg, httpClientProvider,
&oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
@@ -142,7 +143,7 @@ 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(bus.New(), nil, secretsService, &acmock.Mock{})
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/common/some/method", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
proxy.matchedRoute = routes[3]
@@ -154,7 +155,7 @@ 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(bus.New(), nil, secretsService, &acmock.Mock{})
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
proxy.matchedRoute = routes[4]
@@ -165,7 +166,7 @@ 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(bus.New(), nil, secretsService, &acmock.Mock{})
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/body", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
proxy.matchedRoute = routes[5]
@@ -179,7 +180,7 @@ 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(bus.New(), nil, secretsService, &acmock.Mock{})
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/v4/some/method", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
err = proxy.validateRequest()
@@ -188,7 +189,7 @@ 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(bus.New(), nil, secretsService, &acmock.Mock{})
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/admin", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
err = proxy.validateRequest()
@@ -198,7 +199,7 @@ 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(bus.New(), nil, secretsService, &acmock.Mock{})
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/admin", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
err = proxy.validateRequest()
@@ -289,7 +290,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
},
}
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken1", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[0], dsInfo, cfg)
@@ -305,7 +306,7 @@ 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(bus.New(), nil, secretsService, &acmock.Mock{})
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken2", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[1], dsInfo, cfg)
@@ -322,7 +323,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
require.NoError(t, err)
client = newFakeHTTPClient(t, []byte{})
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken1", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[0], dsInfo, cfg)
@@ -344,7 +345,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
ctx := &models.ReqContext{}
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{BuildVersion: "5.3.0"}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
@@ -370,7 +371,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
ctx := &models.ReqContext{}
var routes []*plugins.Route
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
@@ -394,7 +395,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
ctx := &models.ReqContext{}
var routes []*plugins.Route
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
@@ -422,7 +423,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
ctx := &models.ReqContext{}
var pluginRoutes []*plugins.Route
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, pluginRoutes, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
@@ -445,7 +446,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
ctx := &models.ReqContext{}
var routes []*plugins.Route
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/to/folder/", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
@@ -509,7 +510,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
var routes []*plugins.Route
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/to/folder/", &setting.Cfg{}, httpClientProvider, &mockAuthToken, dsService, tracer, secretsService)
require.NoError(t, err)
req, err = http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
@@ -642,7 +643,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
ctx, ds := setUp(t)
var routes []*plugins.Route
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
@@ -660,7 +661,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
})
var routes []*plugins.Route
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
@@ -674,7 +675,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
ctx, ds := setUp(t)
var routes []*plugins.Route
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
@@ -696,7 +697,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
})
var routes []*plugins.Route
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
@@ -721,7 +722,7 @@ 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
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/%2Ftest%2Ftest%2F", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
@@ -745,7 +746,7 @@ 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
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/%2Ftest%2Ftest%2F", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
@@ -770,7 +771,7 @@ func TestNewDataSourceProxy_InvalidURL(t *testing.T) {
require.NoError(t, err)
var routes []*plugins.Route
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
_, err = NewDataSourceProxy(&ds, routes, &ctx, "api/method", &cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService)
require.Error(t, err)
assert.True(t, strings.HasPrefix(err.Error(), `validation of data source URL "://host/root" failed`))
@@ -791,7 +792,7 @@ func TestNewDataSourceProxy_ProtocolLessURL(t *testing.T) {
var routes []*plugins.Route
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
_, err = NewDataSourceProxy(&ds, routes, &ctx, "api/method", &cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
@@ -834,7 +835,7 @@ func TestNewDataSourceProxy_MSSQL(t *testing.T) {
var routes []*plugins.Route
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
p, err := NewDataSourceProxy(&ds, routes, &ctx, "api/method", &cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService)
if tc.err == nil {
require.NoError(t, err)
@@ -861,7 +862,7 @@ func getDatasourceProxiedRequest(t *testing.T, ctx *models.ReqContext, cfg *sett
var routes []*plugins.Route
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
@@ -985,7 +986,7 @@ func runDatasourceAuthTest(t *testing.T, secretsService secrets.Service, test *t
require.NoError(t, err)
var routes []*plugins.Route
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(test.datasource, routes, ctx, "", &setting.Cfg{}, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
@@ -1027,7 +1028,7 @@ func Test_PathCheck(t *testing.T) {
}
ctx, _ := setUp()
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(&models.DataSource{}, routes, ctx, "b", &setting.Cfg{}, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
+5 -38
View File
@@ -2,14 +2,12 @@ package api
import (
"errors"
"fmt"
"net/http"
"strconv"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/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"
@@ -39,7 +37,7 @@ func (hs *HTTPServer) CreateTeam(c *models.ReqContext) response.Response {
// the SignedInUser is an empty struct therefore
// an additional check whether it is an actual user is required
if c.SignedInUser.IsRealUser() {
if err := addOrUpdateTeamMember(c.Req.Context(), hs.TeamPermissionsService, c.SignedInUser.UserId, c.OrgId, team.Id, models.PERMISSION_ADMIN.String()); err != nil {
if err := addOrUpdateTeamMember(c.Req.Context(), hs.teamPermissionsService, c.SignedInUser.UserId, c.OrgId, team.Id, models.PERMISSION_ADMIN.String()); err != nil {
c.Logger.Error("Could not add creator to team", "error", err)
}
} else {
@@ -105,20 +103,6 @@ func (hs *HTTPServer) DeleteTeamByID(c *models.ReqContext) response.Response {
return response.Success("Team deleted")
}
func (hs *HTTPServer) getTeamsAccessControlMetadata(c *models.ReqContext, teamIDs map[string]bool) (map[string]accesscontrol.Metadata, error) {
if hs.AccessControl.IsDisabled() || !c.QueryBool("accesscontrol") {
return nil, nil
}
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser, accesscontrol.Options{ReloadCache: false})
if err != nil || len(userPermissions) == 0 {
hs.log.Warn("could not fetch accesscontrol metadata for teams", "error", err)
return nil, err
}
return accesscontrol.GetResourcesMetadata(c.Req.Context(), userPermissions, "teams", teamIDs), nil
}
// GET /api/teams/search
func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response {
perPage := c.QueryInt("perpage")
@@ -157,8 +141,8 @@ func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response {
teamIDs[strconv.FormatInt(team.Id, 10)] = true
}
metadata, err := hs.getTeamsAccessControlMetadata(c, teamIDs)
if err == nil && len(metadata) != 0 {
metadata := hs.getMultiAccessControlMetadata(c, "teams", teamIDs)
if len(metadata) > 0 {
for _, team := range query.Result.Teams {
team.AccessControl = metadata[strconv.FormatInt(team.Id, 10)]
}
@@ -170,23 +154,6 @@ func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response {
return response.JSON(200, query.Result)
}
func (hs *HTTPServer) getTeamAccessControlMetadata(c *models.ReqContext, teamID int64) (accesscontrol.Metadata, error) {
if hs.AccessControl.IsDisabled() || !c.QueryBool("accesscontrol") {
return nil, nil
}
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser, accesscontrol.Options{ReloadCache: false})
if err != nil || len(userPermissions) == 0 {
hs.log.Warn("could not fetch accesscontrol metadata", "team", teamID, "error", err)
return nil, err
}
key := fmt.Sprintf("%d", teamID)
teamIDs := map[string]bool{key: true}
return accesscontrol.GetResourcesMetadata(c.Req.Context(), userPermissions, "teams", teamIDs)[key], nil
}
// UserFilter returns the user ID used in a filter when querying a team
// 1. If the user is a viewer or editor, this will return the user's ID.
// 2. If the user is an admin, this will return models.FilterIgnoreUser (0)
@@ -227,8 +194,8 @@ func (hs *HTTPServer) GetTeamByID(c *models.ReqContext) response.Response {
return response.Error(500, "Failed to get Team", err)
}
metadata, _ := hs.getTeamAccessControlMetadata(c, query.Result.Id)
query.Result.AccessControl = metadata
// Add accesscontrol metadata
query.Result.AccessControl = hs.getAccessControlMetadata(c, "teams", query.Result.Id)
query.Result.AvatarUrl = dtos.GetGravatarUrlWithDefault(query.Result.Email, query.Result.Name)
return response.JSON(200, &query.Result)
+4 -5
View File
@@ -11,7 +11,6 @@ import (
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
@@ -85,7 +84,7 @@ func (hs *HTTPServer) AddTeamMember(c *models.ReqContext) response.Response {
return response.Error(400, "User is already added to this team", nil)
}
err = addOrUpdateTeamMember(c.Req.Context(), hs.TeamPermissionsService, cmd.UserId, cmd.OrgId, cmd.TeamId, getPermissionName(cmd.Permission))
err = addOrUpdateTeamMember(c.Req.Context(), hs.teamPermissionsService, cmd.UserId, cmd.OrgId, cmd.TeamId, getPermissionName(cmd.Permission))
if err != nil {
return response.Error(500, "Failed to add Member to Team", err)
}
@@ -125,7 +124,7 @@ func (hs *HTTPServer) UpdateTeamMember(c *models.ReqContext) response.Response {
return response.Error(404, "Team member not found.", nil)
}
err = addOrUpdateTeamMember(c.Req.Context(), hs.TeamPermissionsService, userId, orgId, teamId, getPermissionName(cmd.Permission))
err = addOrUpdateTeamMember(c.Req.Context(), hs.teamPermissionsService, userId, orgId, teamId, getPermissionName(cmd.Permission))
if err != nil {
return response.Error(500, "Failed to update team member.", err)
}
@@ -161,7 +160,7 @@ func (hs *HTTPServer) RemoveTeamMember(c *models.ReqContext) response.Response {
}
teamIDString := strconv.FormatInt(teamId, 10)
if _, err := hs.TeamPermissionsService.SetUserPermission(c.Req.Context(), orgId, accesscontrol.User{ID: userId}, teamIDString, ""); err != nil {
if _, err := hs.teamPermissionsService.SetUserPermission(c.Req.Context(), orgId, accesscontrol.User{ID: userId}, teamIDString, ""); err != nil {
if errors.Is(err, models.ErrTeamNotFound) {
return response.Error(404, "Team not found", nil)
}
@@ -178,7 +177,7 @@ func (hs *HTTPServer) RemoveTeamMember(c *models.ReqContext) response.Response {
// addOrUpdateTeamMember adds or updates a team member.
//
// Stubbable by tests.
var addOrUpdateTeamMember = func(ctx context.Context, resourcePermissionService *resourcepermissions.Service, userID, orgID, teamID int64, permission string) error {
var addOrUpdateTeamMember = func(ctx context.Context, resourcePermissionService accesscontrol.PermissionsService, userID, orgID, teamID int64, permission string) error {
teamIDString := strconv.FormatInt(teamID, 10)
if _, err := resourcePermissionService.SetUserPermission(ctx, orgID, accesscontrol.User{ID: userID}, teamIDString, permission); err != nil {
return fmt.Errorf("failed setting permissions for user %d in team %d: %w", userID, teamID, err)
+1 -2
View File
@@ -11,7 +11,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting"
@@ -112,7 +111,7 @@ func TestTeamAPIEndpoint(t *testing.T) {
teamName := "team foo"
addTeamMemberCalled := 0
addOrUpdateTeamMember = func(ctx context.Context, resourcePermissionService *resourcepermissions.Service, userID, orgID, teamID int64,
addOrUpdateTeamMember = func(ctx context.Context, resourcePermissionService accesscontrol.PermissionsService, userID, orgID, teamID int64,
permission string) error {
addTeamMemberCalled++
return nil
+1 -24
View File
@@ -3,14 +3,12 @@ package api
import (
"context"
"errors"
"fmt"
"net/http"
"strconv"
"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/accesscontrol"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
@@ -48,33 +46,12 @@ func (hs *HTTPServer) getUserUserProfile(c *models.ReqContext, userID int64) res
query.Result.IsExternal = true
}
accessControlMetadata, errAC := hs.getGlobalUserAccessControlMetadata(c, userID)
if errAC != nil {
hs.log.Error("Failed to get access control metadata", "error", errAC)
}
query.Result.AccessControl = accessControlMetadata
query.Result.AccessControl = hs.getAccessControlMetadata(c, "global:users", userID)
query.Result.AvatarUrl = dtos.GetGravatarUrl(query.Result.Email)
return response.JSON(200, query.Result)
}
func (hs *HTTPServer) getGlobalUserAccessControlMetadata(c *models.ReqContext, userID int64) (accesscontrol.Metadata, error) {
if hs.AccessControl == nil || hs.AccessControl.IsDisabled() || !c.QueryBool("accesscontrol") {
return nil, nil
}
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser, accesscontrol.Options{ReloadCache: false})
if err != nil || len(userPermissions) == 0 {
return nil, err
}
key := fmt.Sprintf("%d", userID)
userIDs := map[string]bool{key: true}
return accesscontrol.GetResourcesMetadata(c.Req.Context(), userPermissions, "global:users", userIDs)[key], nil
}
// GET /api/users/lookup
func (hs *HTTPServer) GetUserByLoginOrEmail(c *models.ReqContext) response.Response {
query := models.GetUserByLoginQuery{LoginOrEmail: c.Query("loginOrEmail")}
+12 -11
View File
@@ -8,32 +8,33 @@ import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/login/authinfoservice"
authinfostore "github.com/grafana/grafana/pkg/services/login/authinfoservice/database"
"github.com/grafana/grafana/pkg/services/searchusers"
"github.com/grafana/grafana/pkg/services/searchusers/filters"
"github.com/grafana/grafana/pkg/services/secrets/database"
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting"
"golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/services/searchusers"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUserAPIEndpoint_userLoggedIn(t *testing.T) {
settings := setting.NewCfg()
sqlStore := sqlstore.InitTestDB(t)
hs := &HTTPServer{
Cfg: settings,
SQLStore: sqlStore,
Cfg: settings,
SQLStore: sqlStore,
AccessControl: &acmock.Mock{},
}
mockResult := models.SearchUserQueryResult{
+1 -11
View File
@@ -12,9 +12,6 @@ import (
"github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/hooks"
"github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
"github.com/grafana/grafana/pkg/setting"
@@ -59,14 +56,7 @@ func runDbCommand(command func(commandLine utils.CommandLine, sqlStore *sqlstore
return errutil.Wrap("failed to initialize tracer service", err)
}
hooksService := hooks.ProvideService()
ossLicensingService := licensing.ProvideService(cfg, hooksService)
featureManager, err := featuremgmt.ProvideManagerService(cfg, ossLicensingService)
if err != nil {
return errutil.Wrap("failed to initialize feature manager service", err)
}
sqlStore, err := sqlstore.ProvideService(cfg, nil, bus.GetBus(), &migrations.OSSMigrations{}, tracer, featureManager)
sqlStore, err := sqlstore.ProvideService(cfg, nil, bus.GetBus(), &migrations.OSSMigrations{}, tracer)
if err != nil {
return errutil.Wrap("failed to initialize SQL store", err)
}
@@ -43,6 +43,7 @@ var skipPaths = []string{
"public/app/plugins/panel/dashlist/models.cue",
"public/app/plugins/panel/gauge/models.cue",
"public/app/plugins/panel/histogram/models.cue",
"public/app/plugins/panel/heatmap-new/models.cue",
"public/app/plugins/panel/stat/models.cue",
"public/app/plugins/panel/candlestick/models.cue",
"public/app/plugins/panel/state-timeline/models.cue",
-23
View File
@@ -1,23 +0,0 @@
package dashboards
import (
"context"
"github.com/grafana/grafana/pkg/models"
)
// Store is a dashboard store.
type Store interface {
// ValidateDashboardBeforeSave validates a dashboard before save.
ValidateDashboardBeforeSave(dashboard *models.Dashboard, overwrite bool) (bool, error)
// GetFolderByTitle retrieves a dashboard by its title and is used by unified alerting
GetFolderByTitle(orgID int64, title string) (*models.Dashboard, error)
GetProvisionedDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error)
GetProvisionedDataByDashboardUID(orgID int64, dashboardUID string) (*models.DashboardProvisioning, error)
GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error)
SaveProvisionedDashboard(cmd models.SaveDashboardCommand, provisioning *models.DashboardProvisioning) (*models.Dashboard, error)
SaveDashboard(cmd models.SaveDashboardCommand) (*models.Dashboard, error)
UpdateDashboardACLCtx(ctx context.Context, uid int64, items []*models.DashboardAcl) error
// SaveAlerts saves dashboard alerts.
SaveAlerts(ctx context.Context, dashID int64, alerts []*models.Alert) error
}
+2
View File
@@ -45,6 +45,7 @@ type OAuthInfo struct {
TeamsUrl string
AllowSignup bool
Name string
Icon string
TlsClientCert string
TlsClientKey string
TlsClientCa string
@@ -81,6 +82,7 @@ func ProvideService(cfg *setting.Cfg) *SocialService {
HostedDomain: sec.Key("hosted_domain").String(),
AllowSignup: sec.Key("allow_sign_up").MustBool(),
Name: sec.Key("name").MustString(name),
Icon: sec.Key("icon").String(),
TlsClientCert: sec.Key("tls_client_cert").String(),
TlsClientKey: sec.Key("tls_client_key").String(),
TlsClientCa: sec.Key("tls_client_ca").String(),
+21 -19
View File
@@ -191,28 +191,30 @@ func shouldForceLogin(c *models.ReqContext) bool {
return forceLogin
}
func OrgAdminFolderAdminOrTeamAdmin(c *models.ReqContext) {
if c.OrgRole == models.ROLE_ADMIN {
return
}
func OrgAdminFolderAdminOrTeamAdmin(ss sqlstore.Store) func(c *models.ReqContext) {
return func(c *models.ReqContext) {
if c.OrgRole == models.ROLE_ADMIN {
return
}
hasAdminPermissionInFoldersQuery := models.HasAdminPermissionInFoldersQuery{SignedInUser: c.SignedInUser}
if err := sqlstore.HasAdminPermissionInFolders(c.Req.Context(), &hasAdminPermissionInFoldersQuery); err != nil {
c.JsonApiErr(500, "Failed to check if user is a folder admin", err)
}
hasAdminPermissionInFoldersQuery := models.HasAdminPermissionInFoldersQuery{SignedInUser: c.SignedInUser}
if err := ss.HasAdminPermissionInFolders(c.Req.Context(), &hasAdminPermissionInFoldersQuery); err != nil {
c.JsonApiErr(500, "Failed to check if user is a folder admin", err)
}
if hasAdminPermissionInFoldersQuery.Result {
return
}
if hasAdminPermissionInFoldersQuery.Result {
return
}
isAdminOfTeamsQuery := models.IsAdminOfTeamsQuery{SignedInUser: c.SignedInUser}
if err := sqlstore.IsAdminOfTeams(c.Req.Context(), &isAdminOfTeamsQuery); err != nil {
c.JsonApiErr(500, "Failed to check if user is a team admin", err)
}
isAdminOfTeamsQuery := models.IsAdminOfTeamsQuery{SignedInUser: c.SignedInUser}
if err := sqlstore.IsAdminOfTeams(c.Req.Context(), &isAdminOfTeamsQuery); err != nil {
c.JsonApiErr(500, "Failed to check if user is a team admin", err)
}
if isAdminOfTeamsQuery.Result {
return
}
if isAdminOfTeamsQuery.Result {
return
}
accessForbidden(c)
accessForbidden(c)
}
}
+4
View File
@@ -117,6 +117,10 @@ type DashboardWithStaleThumbnail struct {
Slug string
}
type FindDashboardThumbnailCountCommand struct {
Result int64
}
type FindDashboardsWithStaleThumbnailsCommand struct {
IncludeManuallyUploadedThumbnails bool
Theme Theme
-4
View File
@@ -456,7 +456,3 @@ type GetDashboardRefByIdQuery struct {
Id int64
Result *DashboardRef
}
type UnprovisionDashboardCommand struct {
Id int64
}
+5 -1
View File
@@ -11,6 +11,9 @@ import (
"github.com/grafana/grafana/pkg/plugins/backendplugin/provider"
"github.com/grafana/grafana/pkg/plugins/manager/loader"
"github.com/grafana/grafana/pkg/plugins/manager/signature"
"github.com/grafana/grafana/pkg/services/dashboards/database"
service "github.com/grafana/grafana/pkg/services/dashboards/manager"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require"
)
@@ -24,8 +27,9 @@ func TestGetPluginDashboards(t *testing.T) {
},
}
pmCfg := plugins.FromGrafanaCfg(cfg)
dashboardService := service.ProvideDashboardService(database.ProvideDashboardStore(&sqlstore.SQLStore{}))
pm, err := ProvideService(cfg, loader.New(pmCfg, nil,
signature.NewUnsignedAuthorizer(pmCfg), &provider.Service{}))
signature.NewUnsignedAuthorizer(pmCfg), &provider.Service{}), dashboardService)
require.NoError(t, err)
bus.AddHandler("test", func(ctx context.Context, query *models.GetDashboardQuery) error {
+2 -1
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"os"
"path"
@@ -240,7 +241,7 @@ func createPluginBase(pluginJSON plugins.JSONData, class plugins.Class, pluginDi
Class: class,
}
plugin.SetLogger(logger.New("pluginID", plugin.ID))
plugin.SetLogger(log.New(fmt.Sprintf("plugin.%s", plugin.ID)))
setImages(plugin)
return plugin
+20 -17
View File
@@ -9,12 +9,12 @@ import (
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/plugins/backendplugin/instrumentation"
"github.com/grafana/grafana/pkg/plugins/manager/installer"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util/errutil"
)
@@ -30,35 +30,38 @@ var _ plugins.StaticRouteResolver = (*PluginManager)(nil)
var _ plugins.RendererManager = (*PluginManager)(nil)
type PluginManager struct {
cfg *plugins.Cfg
store map[string]*plugins.Plugin
pluginInstaller plugins.Installer
pluginLoader plugins.Loader
pluginsMu sync.RWMutex
pluginPaths map[plugins.Class][]string
log log.Logger
cfg *plugins.Cfg
store map[string]*plugins.Plugin
pluginInstaller plugins.Installer
pluginLoader plugins.Loader
pluginsMu sync.RWMutex
pluginPaths map[plugins.Class][]string
dashboardService dashboards.DashboardService
log log.Logger
}
func ProvideService(grafanaCfg *setting.Cfg, pluginLoader plugins.Loader) (*PluginManager, error) {
func ProvideService(grafanaCfg *setting.Cfg, pluginLoader plugins.Loader, dashboardService dashboards.DashboardService) (*PluginManager, error) {
pm := New(plugins.FromGrafanaCfg(grafanaCfg), map[plugins.Class][]string{
plugins.Core: corePluginPaths(grafanaCfg),
plugins.Bundled: {grafanaCfg.BundledPluginsPath},
plugins.External: append([]string{grafanaCfg.PluginsPath}, pluginSettingPaths(grafanaCfg)...),
}, pluginLoader)
}, pluginLoader, dashboardService)
if err := pm.Init(); err != nil {
return nil, err
}
return pm, nil
}
func New(cfg *plugins.Cfg, pluginPaths map[plugins.Class][]string, pluginLoader plugins.Loader) *PluginManager {
func New(cfg *plugins.Cfg, pluginPaths map[plugins.Class][]string, pluginLoader plugins.Loader,
dashboardService dashboards.DashboardService) *PluginManager {
return &PluginManager{
cfg: cfg,
pluginLoader: pluginLoader,
pluginPaths: pluginPaths,
store: make(map[string]*plugins.Plugin),
log: log.New("plugin.manager"),
pluginInstaller: installer.New(false, cfg.BuildVersion, newInstallerLogger("plugin.installer", true)),
cfg: cfg,
pluginLoader: pluginLoader,
pluginPaths: pluginPaths,
store: make(map[string]*plugins.Plugin),
log: log.New("plugin.manager"),
pluginInstaller: installer.New(false, cfg.BuildVersion, newInstallerLogger("plugin.installer", true)),
dashboardService: dashboardService,
}
}
@@ -7,16 +7,14 @@ import (
"strings"
"testing"
"github.com/grafana/grafana/pkg/infra/tracing"
"go.opentelemetry.io/otel/trace"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
"github.com/grafana/grafana/pkg/plugins/backendplugin/provider"
"github.com/grafana/grafana/pkg/plugins/manager/loader"
"github.com/grafana/grafana/pkg/plugins/manager/signature"
service "github.com/grafana/grafana/pkg/services/dashboards/manager"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/services/searchV2"
@@ -37,6 +35,7 @@ import (
"github.com/grafana/grafana/pkg/tsdb/prometheus"
"github.com/grafana/grafana/pkg/tsdb/tempo"
"github.com/grafana/grafana/pkg/tsdb/testdatasource"
"go.opentelemetry.io/otel/trace"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -95,7 +94,7 @@ func TestPluginManager_int_init(t *testing.T) {
pmCfg := plugins.FromGrafanaCfg(cfg)
pm, err := ProvideService(cfg, loader.New(pmCfg, license, signature.NewUnsignedAuthorizer(pmCfg),
provider.ProvideService(coreRegistry)))
provider.ProvideService(coreRegistry)), &service.DashboardServiceImpl{})
require.NoError(t, err)
verifyCorePluginCatalogue(t, pm)
@@ -120,6 +119,7 @@ func verifyCorePluginCatalogue(t *testing.T, pm *PluginManager) {
"gettingstarted": {},
"graph": {},
"heatmap": {},
"heatmap-new": {},
"histogram": {},
"icon": {},
"live": {},
+7 -2
View File
@@ -12,6 +12,9 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/services/dashboards/database"
service "github.com/grafana/grafana/pkg/services/dashboards/manager"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -466,7 +469,8 @@ func TestPluginManager_lifecycle_unmanaged(t *testing.T) {
func createManager(t *testing.T, cbs ...func(*PluginManager)) *PluginManager {
t.Helper()
pm := New(&plugins.Cfg{}, nil, &fakeLoader{})
dashboardService := service.ProvideDashboardService(database.ProvideDashboardStore(&sqlstore.SQLStore{}))
pm := New(&plugins.Cfg{}, nil, &fakeLoader{}, dashboardService)
for _, cb := range cbs {
cb(pm)
@@ -520,7 +524,8 @@ func newScenario(t *testing.T, managed bool, fn func(t *testing.T, ctx *managerS
cfg.Azure.ManagedIdentityClientId = "client-id"
loader := &fakeLoader{}
manager := New(cfg, nil, loader)
dashboardService := service.ProvideDashboardService(database.ProvideDashboardStore(&sqlstore.SQLStore{}))
manager := New(cfg, nil, loader, dashboardService)
manager.pluginLoader = loader
ctx := &managerScenarioCtx{
manager: manager,
+10 -2
View File
@@ -27,13 +27,15 @@ import (
"github.com/grafana/grafana/pkg/plugins/manager"
"github.com/grafana/grafana/pkg/plugins/manager/loader"
"github.com/grafana/grafana/pkg/plugins/plugincontext"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourceservices"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/auth/jwt"
"github.com/grafana/grafana/pkg/services/cleanup"
"github.com/grafana/grafana/pkg/services/contexthandler"
"github.com/grafana/grafana/pkg/services/dashboardimport"
dashboardimportservice "github.com/grafana/grafana/pkg/services/dashboardimport/service"
"github.com/grafana/grafana/pkg/services/dashboards"
dashboardstore "github.com/grafana/grafana/pkg/services/dashboards/database"
dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/manager"
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
"github.com/grafana/grafana/pkg/services/datasourceproxy"
"github.com/grafana/grafana/pkg/services/datasources"
@@ -199,7 +201,13 @@ var wireBasicSet = wire.NewSet(
wire.Bind(new(teamguardian.TeamGuardian), new(*teamguardianManager.Service)),
featuremgmt.ProvideManagerService,
featuremgmt.ProvideToggles,
resourceservices.ProvideResourceServices,
dashboardservice.ProvideDashboardService,
dashboardservice.ProvideFolderService,
dashboardstore.ProvideDashboardStore,
wire.Bind(new(dashboards.DashboardService), new(*dashboardservice.DashboardServiceImpl)),
wire.Bind(new(dashboards.DashboardProvisioningService), new(*dashboardservice.DashboardServiceImpl)),
wire.Bind(new(dashboards.FolderService), new(*dashboardservice.FolderServiceImpl)),
wire.Bind(new(dashboards.Store), new(*dashboardstore.DashboardStore)),
dashboardimportservice.ProvideService,
wire.Bind(new(dashboardimport.Service), new(*dashboardimportservice.ImportDashboardService)),
plugindashboards.ProvideService,
+2
View File
@@ -77,6 +77,8 @@ var wireExtsBasicSet = wire.NewSet(
wire.Bind(new(ldap.Groups), new(*ldap.OSSGroups)),
api.ProvideDatasourcePermissionsService,
wire.Bind(new(api.DatasourcePermissionsService), new(*api.OSSDatasourcePermissionsService)),
ossaccesscontrol.ProvidePermissionsServices,
wire.Bind(new(accesscontrol.PermissionsServices), new(*ossaccesscontrol.PermissionsService)),
)
var wireExtsSet = wire.NewSet(
+19 -12
View File
@@ -37,7 +37,12 @@ type PermissionsProvider interface {
GetUserPermissions(ctx context.Context, query GetUserPermissionsQuery) ([]*Permission, error)
}
type ResourcePermissionsService interface {
type PermissionsServices interface {
GetTeamService() PermissionsService
GetDataSourceService() PermissionsService
}
type PermissionsService interface {
// GetPermissions returns all permissions for given resourceID
GetPermissions(ctx context.Context, orgID int64, resourceID string) ([]ResourcePermission, error)
// SetUserPermission sets permission on resource for a user
@@ -146,7 +151,7 @@ func addActionToMetadata(allMetadata map[string]Metadata, action, id string) map
}
// GetResourcesMetadata returns a map of accesscontrol metadata, listing for each resource, users available actions
func GetResourcesMetadata(ctx context.Context, permissions []*Permission, resource string, resourceIDs map[string]bool) map[string]Metadata {
func GetResourcesMetadata(ctx context.Context, permissions map[string][]string, resource string, ids map[string]bool) map[string]Metadata {
allScope := GetResourceAllScope(resource)
allIDScope := GetResourceAllIDScope(resource)
@@ -157,16 +162,18 @@ func GetResourcesMetadata(ctx context.Context, permissions []*Permission, resour
// Loop through permissions once
result := map[string]Metadata{}
for _, p := range permissions {
if p.Scope == "*" || p.Scope == allScope || p.Scope == allIDScope {
// Add global action to all resources
for id := range resourceIDs {
result = addActionToMetadata(result, p.Action, id)
}
} else {
if len(p.Scope) > idIndex && strings.HasPrefix(p.Scope, idPrefix) && resourceIDs[p.Scope[idIndex:]] {
// Add action to a specific resource
result = addActionToMetadata(result, p.Action, p.Scope[idIndex:])
for action, scopes := range permissions {
for _, scope := range scopes {
if scope == "*" || scope == allScope || scope == allIDScope {
// Add global action to all resources
for id := range ids {
result = addActionToMetadata(result, action, id)
}
} else {
if len(scope) > idIndex && strings.HasPrefix(scope, idPrefix) && ids[scope[idIndex:]] {
// Add action to a specific resource
result = addActionToMetadata(result, action, scope[idIndex:])
}
}
}
}
@@ -9,17 +9,17 @@ import (
"github.com/stretchr/testify/assert"
)
func setupTestEnv(b *testing.B, resourceCount, permissionPerResource int) ([]*Permission, map[string]bool) {
res := make([]*Permission, resourceCount*permissionPerResource)
func setupTestEnv(b *testing.B, resourceCount, permissionPerResource int) (map[string][]string, map[string]bool) {
res := map[string][]string{}
ids := make(map[string]bool, resourceCount)
for r := 0; r < resourceCount; r++ {
for p := 0; p < permissionPerResource; p++ {
perm := Permission{Action: fmt.Sprintf("resources:action%v", p), Scope: fmt.Sprintf("resources:id:%v", r)}
id := r*permissionPerResource + p
res[id] = &perm
for p := 0; p < permissionPerResource; p++ {
action := fmt.Sprintf("resources:action%v", p)
for r := 0; r < resourceCount; r++ {
scope := fmt.Sprintf("resources:id:%v", r)
res[action] = append(res[action], scope)
ids[fmt.Sprintf("%d", r)] = true
}
ids[fmt.Sprintf("%d", r)] = true
}
return res, ids
@@ -40,23 +40,23 @@ func benchGetMetadata(b *testing.B, resourceCount, permissionPerResource int) {
}
// Lots of permissions
func BenchmarkGetResourcesMetadata_10_1000(b *testing.B) { benchGetMetadata(b, 10, 1000) } // ~0.0017s/op
func BenchmarkGetResourcesMetadata_10_10000(b *testing.B) { benchGetMetadata(b, 10, 10000) } // ~0.016s/op
func BenchmarkGetResourcesMetadata_10_100000(b *testing.B) { benchGetMetadata(b, 10, 100000) } // ~0.17s/op
func BenchmarkGetResourcesMetadata_10_1000(b *testing.B) { benchGetMetadata(b, 10, 1000) } // ~0.0022s/op
func BenchmarkGetResourcesMetadata_10_10000(b *testing.B) { benchGetMetadata(b, 10, 10000) } // ~0.019s/op
func BenchmarkGetResourcesMetadata_10_100000(b *testing.B) { benchGetMetadata(b, 10, 100000) } // ~0.25s/op
func BenchmarkGetResourcesMetadata_10_1000000(b *testing.B) {
if testing.Short() {
b.Skip("Skipping benchmark in short mode")
}
benchGetMetadata(b, 10, 1000000)
} // ~3.89s/op
} // ~5.8s/op
// Lots of resources
func BenchmarkGetResourcesMetadata_1000_10(b *testing.B) { benchGetMetadata(b, 1000, 10) } // ~0,0023s/op
func BenchmarkGetResourcesMetadata_10000_10(b *testing.B) { benchGetMetadata(b, 10000, 10) } // ~0.021s/op
func BenchmarkGetResourcesMetadata_100000_10(b *testing.B) { benchGetMetadata(b, 100000, 10) } // ~0.22s/op
func BenchmarkGetResourcesMetadata_10000_10(b *testing.B) { benchGetMetadata(b, 10000, 10) } // ~0.022s/op
func BenchmarkGetResourcesMetadata_100000_10(b *testing.B) { benchGetMetadata(b, 100000, 10) } // ~0.26s/op
func BenchmarkGetResourcesMetadata_1000000_10(b *testing.B) {
if testing.Short() {
b.Skip("Skipping benchmark in short mode")
}
benchGetMetadata(b, 1000000, 10)
} // ~2.8s/op
} // ~4.1s/op
@@ -12,7 +12,7 @@ func TestGetResourcesMetadata(t *testing.T) {
desc string
resource string
resourcesIDs map[string]bool
permissions []*Permission
permissions map[string][]string
expected map[string]Metadata
}{
{
@@ -24,10 +24,10 @@ func TestGetResourcesMetadata(t *testing.T) {
{
desc: "Should return no permission for resources 1,2,3 given the user has permissions for 4 only",
resource: "resources",
permissions: []*Permission{
{Action: "resources:action1", Scope: Scope("resources", "id", "4")},
{Action: "resources:action2", Scope: Scope("resources", "id", "4")},
{Action: "resources:action3", Scope: Scope("resources", "id", "4")},
permissions: map[string][]string{
"resources:action1": {Scope("resources", "id", "4")},
"resources:action2": {Scope("resources", "id", "4")},
"resources:action3": {Scope("resources", "id", "4")},
},
resourcesIDs: map[string]bool{"1": true, "2": true, "3": true},
expected: map[string]Metadata{},
@@ -35,10 +35,10 @@ func TestGetResourcesMetadata(t *testing.T) {
{
desc: "Should only return permissions for resources 1 and 2, given the user has no permissions for 3",
resource: "resources",
permissions: []*Permission{
{Action: "resources:action1", Scope: Scope("resources", "id", "1")},
{Action: "resources:action2", Scope: Scope("resources", "id", "2")},
{Action: "resources:action3", Scope: Scope("resources", "id", "2")},
permissions: map[string][]string{
"resources:action1": {Scope("resources", "id", "1")},
"resources:action2": {Scope("resources", "id", "2")},
"resources:action3": {Scope("resources", "id", "2")},
},
resourcesIDs: map[string]bool{"1": true, "2": true, "3": true},
expected: map[string]Metadata{
@@ -49,13 +49,13 @@ func TestGetResourcesMetadata(t *testing.T) {
{
desc: "Should return permissions with global scopes for resources 1,2,3",
resource: "resources",
permissions: []*Permission{
{Action: "resources:action4", Scope: Scope("resources", "id", "*")},
{Action: "resources:action5", Scope: Scope("resources", "*")},
{Action: "resources:action6", Scope: "*"},
{Action: "resources:action1", Scope: Scope("resources", "id", "1")},
{Action: "resources:action2", Scope: Scope("resources", "id", "2")},
{Action: "resources:action3", Scope: Scope("resources", "id", "2")},
permissions: map[string][]string{
"resources:action1": {Scope("resources", "id", "1")},
"resources:action2": {Scope("resources", "id", "2")},
"resources:action3": {Scope("resources", "id", "2")},
"resources:action4": {Scope("resources", "id", "*")},
"resources:action5": {Scope("resources", "*")},
"resources:action6": {"*"},
},
resourcesIDs: map[string]bool{"1": true, "2": true, "3": true},
expected: map[string]Metadata{
@@ -67,11 +67,10 @@ func TestGetResourcesMetadata(t *testing.T) {
{
desc: "Should correctly filter out irrelevant permissions for resources 1,2,3",
resource: "resources",
permissions: []*Permission{
{Action: "resources:action1", Scope: Scope("resources", "id", "1")},
{Action: "otherresources:action1", Scope: Scope("resources", "id", "1")},
{Action: "resources:action2", Scope: Scope("otherresources", "id", "*")},
{Action: "otherresources:action1", Scope: Scope("otherresources", "id", "*")},
permissions: map[string][]string{
"resources:action1": {Scope("resources", "id", "1")},
"resources:action2": {Scope("otherresources", "id", "*")},
"otherresources:action1": {Scope("resources", "id", "1"), Scope("otherresources", "id", "*")},
},
resourcesIDs: map[string]bool{"1": true, "2": true, "3": true},
expected: map[string]Metadata{
@@ -81,9 +80,8 @@ func TestGetResourcesMetadata(t *testing.T) {
{
desc: "Should correctly handle permissions with multilayer scope",
resource: "resources:sub",
permissions: []*Permission{
{Action: "resources:action1", Scope: Scope("resources", "sub", "id", "1")},
{Action: "resources:action1", Scope: Scope("resources", "sub", "id", "123")},
permissions: map[string][]string{
"resources:action1": {Scope("resources", "sub", "id", "1"), Scope("resources", "sub", "id", "123")},
},
resourcesIDs: map[string]bool{"1": true, "123": true},
expected: map[string]Metadata{
@@ -0,0 +1,27 @@
package mock
import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
)
var _ accesscontrol.PermissionsServices = new(PermissionsServicesMock)
func NewPermissionsServicesMock() *PermissionsServicesMock {
return &PermissionsServicesMock{
teams: &MockPermissionsService{},
datasources: &MockPermissionsService{},
}
}
type PermissionsServicesMock struct {
teams *MockPermissionsService
datasources *MockPermissionsService
}
func (p PermissionsServicesMock) GetTeamService() accesscontrol.PermissionsService {
return p.teams
}
func (p PermissionsServicesMock) GetDataSourceService() accesscontrol.PermissionsService {
return p.datasources
}
@@ -0,0 +1,40 @@
package mock
import (
"context"
"github.com/stretchr/testify/mock"
"github.com/grafana/grafana/pkg/services/accesscontrol"
)
var _ accesscontrol.PermissionsService = new(MockPermissionsService)
type MockPermissionsService struct {
mock.Mock
}
func (m *MockPermissionsService) GetPermissions(ctx context.Context, orgID int64, resourceID string) ([]accesscontrol.ResourcePermission, error) {
mockedArgs := m.Called(ctx, orgID, resourceID)
return mockedArgs.Get(0).([]accesscontrol.ResourcePermission), mockedArgs.Error(1)
}
func (m *MockPermissionsService) SetUserPermission(ctx context.Context, orgID int64, user accesscontrol.User, resourceID, permission string) (*accesscontrol.ResourcePermission, error) {
mockedArgs := m.Called(ctx, orgID, user, resourceID, permission)
return mockedArgs.Get(0).(*accesscontrol.ResourcePermission), mockedArgs.Error(1)
}
func (m *MockPermissionsService) SetTeamPermission(ctx context.Context, orgID, teamID int64, resourceID, permission string) (*accesscontrol.ResourcePermission, error) {
mockedArgs := m.Called(ctx, orgID, teamID, resourceID, permission)
return mockedArgs.Get(0).(*accesscontrol.ResourcePermission), mockedArgs.Error(1)
}
func (m *MockPermissionsService) SetBuiltInRolePermission(ctx context.Context, orgID int64, builtInRole, resourceID, permission string) (*accesscontrol.ResourcePermission, error) {
mockedArgs := m.Called(ctx, orgID, builtInRole, resourceID, permission)
return mockedArgs.Get(0).(*accesscontrol.ResourcePermission), mockedArgs.Error(1)
}
func (m *MockPermissionsService) SetPermissions(ctx context.Context, orgID int64, resourceID string, commands ...accesscontrol.SetResourcePermissionCommand) ([]accesscontrol.ResourcePermission, error) {
mockedArgs := m.Called(ctx, orgID, resourceID, commands)
return mockedArgs.Get(0).([]accesscontrol.ResourcePermission), mockedArgs.Error(1)
}

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