Merge remote-tracking branch 'origin/main' into guicaulada/unified-secrets

This commit is contained in:
Guilherme Caulada
2022-04-06 12:57:20 -03:00
248 changed files with 6230 additions and 2069 deletions
-15
View File
@@ -230,21 +230,6 @@ exports[`no enzyme tests`] = {
"public/app/features/explore/LiveLogs.test.tsx:1667605379": [
[2, 17, 13, "RegExp match", "2409514259"]
],
"public/app/features/explore/RichHistory/RichHistory.test.tsx:409631018": [
[1, 17, 13, "RegExp match", "2409514259"]
],
"public/app/features/explore/RichHistory/RichHistoryCard.test.tsx:689438177": [
[1, 19, 13, "RegExp match", "2409514259"]
],
"public/app/features/explore/RichHistory/RichHistoryContainer.test.tsx:396471778": [
[1, 17, 13, "RegExp match", "2409514259"]
],
"public/app/features/explore/RichHistory/RichHistoryQueriesTab.test.tsx:3436519226": [
[1, 17, 13, "RegExp match", "2409514259"]
],
"public/app/features/explore/RichHistory/RichHistorySettings.test.tsx:538589654": [
[1, 17, 13, "RegExp match", "2409514259"]
],
"public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx:3948011811": [
[1, 17, 13, "RegExp match", "2409514259"]
],
+88 -71
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.30/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -114,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.30/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -356,7 +356,7 @@ services:
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -420,7 +420,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -503,7 +503,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -565,8 +565,6 @@ trigger:
branch: main
event:
- push
repo:
- grafana/grafana
type: docker
volumes:
- host:
@@ -585,7 +583,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -668,8 +666,6 @@ trigger:
branch: main
event:
- push
repo:
- grafana/grafana
type: docker
volumes:
- host:
@@ -688,7 +684,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -727,6 +723,8 @@ steps:
paths:
include:
- .drone.yml
repo:
- grafana/grafana
- image: grafana/drone-downstream
name: trigger-enterprise-downstream
settings:
@@ -903,7 +901,8 @@ steps:
- commands:
- printenv GCP_KEY | base64 -d > /tmp/gcpkey.json
- gcloud auth activate-service-account --key-file=/tmp/gcpkey.json
- gsutil -m rsync -d -r ./packages/grafana-ui/dist/storybook gs://$${PRERELEASE_BUCKET}/artifacts/storybook/canary
- gsutil -m rm -r gs://$${PRERELEASE_BUCKET}/artifacts/storybook/canary && gsutil
-m cp -r ./packages/grafana-ui/dist/storybook/* gs://$${PRERELEASE_BUCKET}/artifacts/storybook/canary
depends_on:
- build-storybook
- end-to-end-tests-dashboards-suite
@@ -917,6 +916,9 @@ steps:
from_secret: prerelease_bucket
image: grafana/grafana-ci-deploy:1.3.1
name: store-storybook
when:
repo:
- grafana/grafana
- commands:
- yarn wait-on http://$HOST:$PORT
- pa11y-ci --config .pa11yci.conf.js --json > pa11y-ci-results.json
@@ -940,6 +942,9 @@ steps:
failure: ignore
image: grafana/build-container:1.5.3
name: publish-frontend-metrics
when:
repo:
- grafana/grafana
- commands:
- ls dist/*.tar.gz*
- cp dist/*.tar.gz* packaging/docker/
@@ -989,6 +994,9 @@ steps:
volumes:
- name: docker
path: /var/run/docker.sock
when:
repo:
- grafana/grafana
- commands:
- ./bin/grabpl artifacts docker publish --dockerhub-repo grafana-oss --base alpine
--base ubuntu --arch amd64 --arch arm64 --arch armv7
@@ -1007,6 +1015,9 @@ steps:
volumes:
- name: docker
path: /var/run/docker.sock
when:
repo:
- grafana/grafana
- commands:
- ./scripts/circle-release-canary-packages.sh
depends_on:
@@ -1019,6 +1030,9 @@ steps:
from_secret: npm_token
image: grafana/build-container:1.5.3
name: release-canary-npm-packages
when:
repo:
- grafana/grafana
- commands:
- ./bin/grabpl upload-packages --edition oss --packages-bucket grafana-downloads
depends_on:
@@ -1033,6 +1047,9 @@ steps:
from_secret: prerelease_bucket
image: grafana/grafana-ci-deploy:1.3.1
name: upload-packages
when:
repo:
- grafana/grafana
- commands:
- ./bin/grabpl upload-cdn --edition oss --src-bucket "grafana-static-assets"
depends_on:
@@ -1044,12 +1061,13 @@ steps:
from_secret: prerelease_bucket
image: grafana/grafana-ci-deploy:1.3.1
name: upload-cdn-assets
when:
repo:
- grafana/grafana
trigger:
branch: main
event:
- push
repo:
- grafana/grafana
type: docker
volumes:
- host:
@@ -1094,7 +1112,7 @@ services:
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -1132,8 +1150,6 @@ trigger:
branch: main
event:
- push
repo:
- grafana/grafana
type: docker
volumes:
- host:
@@ -1164,7 +1180,7 @@ steps:
name: identify-runner
- commands:
- $$ProgressPreference = "SilentlyContinue"
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/windows/grabpl.exe
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/windows/grabpl.exe
-OutFile grabpl.exe
image: grafana/ci-wix:0.1.1
name: initialize
@@ -1251,7 +1267,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -1319,8 +1335,6 @@ trigger:
branch: main
event:
- push
repo:
- grafana/grafana
status:
- failure
type: docker
@@ -1337,7 +1351,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -1580,8 +1594,10 @@ steps:
- commands:
- printenv GCP_KEY | base64 -d > /tmp/gcpkey.json
- gcloud auth activate-service-account --key-file=/tmp/gcpkey.json
- gsutil -m rsync -d -r ./packages/grafana-ui/dist/storybook gs://$${PRERELEASE_BUCKET}/artifacts/storybook/latest
- gsutil -m rsync -d -r ./packages/grafana-ui/dist/storybook gs://$${PRERELEASE_BUCKET}/artifacts/storybook/${DRONE_TAG}
- gsutil -m rm -r gs://$${PRERELEASE_BUCKET}/artifacts/storybook/latest && gsutil
-m cp -r ./packages/grafana-ui/dist/storybook/* gs://$${PRERELEASE_BUCKET}/artifacts/storybook/latest
- gsutil -m rm -r gs://$${PRERELEASE_BUCKET}/artifacts/storybook/${DRONE_TAG} &&
gsutil -m cp -r ./packages/grafana-ui/dist/storybook/* gs://$${PRERELEASE_BUCKET}/artifacts/storybook/${DRONE_TAG}
depends_on:
- build-storybook
- end-to-end-tests-dashboards-suite
@@ -1595,16 +1611,10 @@ steps:
from_secret: prerelease_bucket
image: grafana/grafana-ci-deploy:1.3.1
name: store-storybook
- commands:
- ./scripts/build/build-npm-packages.sh ${DRONE_TAG}
depends_on:
- store-storybook
image: grafana/build-container:1.5.3
name: build-npm-packages
- commands:
- ./bin/grabpl artifacts npm store --tag ${DRONE_TAG}
depends_on:
- build-npm-packages
- build-frontend-packages
environment:
GCP_KEY:
from_secret: gcp_key
@@ -1645,7 +1655,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -1766,7 +1776,7 @@ services:
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -1850,7 +1860,7 @@ steps:
name: identify-runner
- commands:
- $$ProgressPreference = "SilentlyContinue"
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/windows/grabpl.exe
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/windows/grabpl.exe
-OutFile grabpl.exe
image: grafana/ci-wix:0.1.1
name: initialize
@@ -1909,7 +1919,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2170,6 +2180,17 @@ steps:
from_secret: prerelease_bucket
image: grafana/grafana-ci-deploy:1.3.1
name: upload-packages
- commands:
- ./bin/grabpl artifacts npm store --tag ${DRONE_TAG}
depends_on:
- build-frontend-packages
environment:
GCP_KEY:
from_secret: gcp_key
PRERELEASE_BUCKET:
from_secret: prerelease_bucket
image: grafana/grafana-ci-deploy:1.3.1
name: store-npm-packages
- commands:
- ./bin/grabpl package --jobs 8 --edition enterprise2 --github-token $${GITHUB_TOKEN}
--sign ${DRONE_TAG}
@@ -2252,7 +2273,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2425,7 +2446,7 @@ services:
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2553,7 +2574,7 @@ steps:
name: identify-runner
- commands:
- $$ProgressPreference = "SilentlyContinue"
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/windows/grabpl.exe
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/windows/grabpl.exe
-OutFile grabpl.exe
- git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git"
- cd grafana-enterprise
@@ -2628,7 +2649,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2706,7 +2727,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2767,7 +2788,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2846,7 +2867,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2908,7 +2929,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2944,7 +2965,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2991,7 +3012,7 @@ steps:
name: initialize
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -3041,7 +3062,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -3104,7 +3125,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -3322,6 +3343,9 @@ steps:
from_secret: prerelease_bucket
image: grafana/grafana-ci-deploy:1.3.1
name: upload-cdn-assets
when:
repo:
- grafana/grafana
- commands:
- ./bin/grabpl upload-packages --edition oss --packages-bucket grafana-downloads
depends_on:
@@ -3336,11 +3360,12 @@ steps:
from_secret: prerelease_bucket
image: grafana/grafana-ci-deploy:1.3.1
name: upload-packages
when:
repo:
- grafana/grafana
trigger:
ref:
- refs/heads/v[0-9]*
repo:
- grafana/grafana
type: docker
volumes:
- host:
@@ -3365,7 +3390,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -3441,8 +3466,6 @@ steps:
trigger:
ref:
- refs/heads/v[0-9]*
repo:
- grafana/grafana
type: docker
volumes:
- host:
@@ -3481,7 +3504,7 @@ services:
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -3528,8 +3551,6 @@ steps:
trigger:
ref:
- refs/heads/v[0-9]*
repo:
- grafana/grafana
type: docker
volumes:
- host:
@@ -3560,7 +3581,7 @@ steps:
name: identify-runner
- commands:
- $$ProgressPreference = "SilentlyContinue"
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/windows/grabpl.exe
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/windows/grabpl.exe
-OutFile grabpl.exe
image: grafana/ci-wix:0.1.1
name: initialize
@@ -3586,8 +3607,6 @@ steps:
trigger:
ref:
- refs/heads/v[0-9]*
repo:
- grafana/grafana
type: docker
volumes:
- host:
@@ -3610,7 +3629,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -3859,6 +3878,9 @@ steps:
from_secret: prerelease_bucket
image: grafana/grafana-ci-deploy:1.3.1
name: upload-cdn-assets
when:
repo:
- grafana/grafana
- commands:
- ./bin/grabpl upload-packages --edition enterprise --packages-bucket grafana-downloads
depends_on:
@@ -3870,6 +3892,9 @@ steps:
from_secret: prerelease_bucket
image: grafana/grafana-ci-deploy:1.3.1
name: upload-packages
when:
repo:
- grafana/grafana
- commands:
- ./bin/grabpl package --jobs 8 --edition enterprise2 --build-id ${DRONE_BUILD_NUMBER}
--variants linux-amd64 --sign
@@ -3917,8 +3942,6 @@ steps:
trigger:
ref:
- refs/heads/v[0-9]*
repo:
- grafana/grafana
type: docker
volumes:
- host:
@@ -3947,7 +3970,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -4062,8 +4085,6 @@ steps:
trigger:
ref:
- refs/heads/v[0-9]*
repo:
- grafana/grafana
type: docker
volumes:
- host:
@@ -4112,7 +4133,7 @@ services:
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -4196,8 +4217,6 @@ steps:
trigger:
ref:
- refs/heads/v[0-9]*
repo:
- grafana/grafana
type: docker
volumes:
- host:
@@ -4232,7 +4251,7 @@ steps:
name: identify-runner
- commands:
- $$ProgressPreference = "SilentlyContinue"
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/windows/grabpl.exe
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.32/windows/grabpl.exe
-OutFile grabpl.exe
- git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git"
- cd grafana-enterprise
@@ -4278,8 +4297,6 @@ steps:
trigger:
ref:
- refs/heads/v[0-9]*
repo:
- grafana/grafana
type: docker
volumes:
- host:
@@ -4431,6 +4448,6 @@ kind: secret
name: gcp_upload_artifacts_key
---
kind: signature
hmac: 430843c058dc4f40a3f881e13fc61ab12f5b6dc823f99067e56ab49fc1bdd459
hmac: ec763e70ae08f79845dd4a59c56e6d08ef8e6b39a1f59663aabcc6d9fe758287
...
+4
View File
@@ -164,3 +164,7 @@ lerna.json @grafana/frontend-ops
# Cloud middleware
/grafana-mixin/ @grafana/hosted-grafana-team
# Grafana authentication and authorization
/pkg/services/accesscontrol @grafana/grafana-enterprise-operations-team
/pkg/services/serviceaccounts @grafana/grafana-enterprise-operations-team
@@ -0,0 +1,17 @@
name: Cloud data sources test code coverage
on:
pull_request:
paths:
- 'pkg/tsdb/azuremonitor/**'
- 'pkg/tsdb/cloudwatch/**'
- 'pkg/tsdb/cloudmonitoring/**'
- 'public/app/plugins/datasource/grafana-azure-monitor-datasource/**'
- 'public/app/plugins/datasource/cloudwatch/**'
- 'public/app/plugins/datasource/cloud-monitoring/**'
branches-ignore:
- dependabot/**
- backport-**
jobs:
workflow-call:
uses: grafana/code-coverage/.github/workflows/code-coverage.yml@v0.1.2
+3 -3
View File
@@ -34,7 +34,7 @@ packageExtensions:
doctrine@3.0.0:
dependencies:
assert: 2.0.0
moveable@0.27.3:
moveable@0.28.0:
dependencies:
"@daybrush/utils": 1.6.0
framework-utils: ^1.1.0
@@ -49,13 +49,13 @@ packageExtensions:
react-compat-css-styled@1.0.8:
dependencies:
react-simple-compat: 1.2.1
react-compat-moveable@0.15.2:
react-compat-moveable@0.16.0:
dependencies:
"@egjs/agent": ^2.2.1
"@egjs/children-differ": ^1.0.1
"@scena/matrix": 1.1.1
css-to-mat: ^1.0.3
gesto: ^1.4.0
gesto: ^1.7.0
overlap-area: ^1.0.0
react-simple-compat: 1.2.1
peerDependencies:
+10 -3
View File
@@ -193,12 +193,19 @@ reporting_enabled = true
reporting_distributor = grafana-labs
# Set to false to disable all checks to https://grafana.com
# for new versions (grafana itself and plugins), check is used
# in some UI views to notify that grafana or plugin update exists
# for new versions of grafana. The check is used
# in some UI views to notify that a grafana update exists.
# This option does not cause any auto updates, nor send any information
# only a GET request to https://grafana.com to get latest versions
# only a GET request to https://raw.githubusercontent.com/grafana/grafana/main/latest.json to get the latest version.
check_for_updates = true
# Set to false to disable all checks to https://grafana.com
# for new versions of plugins. The check is used
# in some UI views to notify that a plugin update exists.
# This option does not cause any auto updates, nor send any information
# only a GET request to https://grafana.com to get the latest versions.
check_for_plugin_updates = true
# Google Analytics universal tracking code, only enabled if you specify an id here
google_analytics_ua_id =
+11 -4
View File
@@ -198,13 +198,20 @@
# The name of the distributor of the Grafana instance. Ex hosted-grafana, grafana-labs
;reporting_distributor = grafana-labs
# Set to false to disable all checks to https://grafana.net
# for new versions (grafana itself and plugins), check is used
# in some UI views to notify that grafana or plugin update exists
# Set to false to disable all checks to https://grafana.com
# for new versions of grafana. The check is used
# in some UI views to notify that a grafana update exists.
# This option does not cause any auto updates, nor send any information
# only a GET request to http://grafana.com to get latest versions
# only a GET request to https://raw.githubusercontent.com/grafana/grafana/main/latest.json to get the latest version.
;check_for_updates = true
# Set to false to disable all checks to https://grafana.com
# for new versions of plugins. The check is used
# in some UI views to notify that a plugin update exists.
# This option does not cause any auto updates, nor send any information
# only a GET request to https://grafana.com to get the latest versions.
;check_for_plugin_updates = true
# Google Analytics universal tracking code, only enabled if you specify an id here
;google_analytics_ua_id =
+2 -2
View File
@@ -212,10 +212,10 @@ of the sentence. When you write in passive voice, the recipient of the action (a
Active-voice sentences are more direct and clearly identify _who_ is doing _what_. Not all tasks are completed by a user; sometimes the system can also be a performer (and by extension, the subject of the sentence). When you write in active voice you clearly make that distinction, which results in more engaging and less wordy content.
| Use (active) | Avoid (passive) |
| ----------------------------------------------------- | -------------------------------------------------------------------- | --- |
| ----------------------------------------------------- | -------------------------------------------------------------------- |
| After you upgrade the software, restart the computer. | After the software has been upgraded, the computer can be restarted. |
| Click **OK** to save the dashboard. | The dashboard is saved when the **OK** button is clicked. |
| Create a dashboard. | A dashboard is created by you. | . |
| Create a dashboard. | A dashboard is created by you. |
### Avoid obscure non-English words and abbreviations
+7 -1
View File
@@ -461,7 +461,13 @@ value is `true`.
### check_for_updates
Set to false to disable all checks to https://grafana.com for new versions of installed plugins and to the Grafana GitHub repository to check for a newer version of Grafana. The version information is used in some UI views to notify that a new Grafana update or a plugin update exists. This option does not cause any auto updates, nor send any sensitive information. The check is run every 10 minutes.
Set to false, disables checking for new versions of Grafana from Grafana's GitHub repository. When enabled, the check for a new version runs every 10 minutes. It will notify, via the UI, when a new version is available. The check itself will not prompt any auto-updates of the Grafana software, nor will it send any sensitive information.
### check_for_plugin_updates
> **Note**: Available in Grafana v8.5.0 and later versions.
Set to false disables checking for new versions of installed plugins from https://grafana.com. When enabled, the check for a new plugin runs every 10 minutes. It will notify, via the UI, when a new plugin update exists. The check itself will not prompt any auto-updates of the plugin, nor will it send any sensitive information.
### google_analytics_ua_id
+1 -1
View File
@@ -10,7 +10,7 @@ Alerts allow you to learn about problems in your systems moments after they occu
Grafana 8.0 introduced new and improved alerting that centralizes alerting information in a single, searchable view. It allows you to:
- Create and manage Grafana alerts
- Create and manage Cortex and Loki managed alerts
- Create and manage Grafana Mimir and Loki managed alerts
- View alerting information from Prometheus and Alertmanager compatible data sources
Grafana alerting is enabled by default for new OSS installations. For older installations, it is still an [opt-in]({{< relref "./unified-alerting/opt-in.md" >}}) feature.
@@ -11,7 +11,7 @@ Grafana 8.0 has new and improved alerting that centralizes alerting information
When Grafana alerting is enabled, you can:
- [Create Grafana managed alerting rules]({{< relref "alerting-rules/create-grafana-managed-rule.md" >}})
- [Create Cortex or Loki managed alerting rules]({{< relref "alerting-rules/create-cortex-loki-managed-rule.md" >}})
- [Create Grafana Mimir or Loki managed alerting rules]({{< relref "alerting-rules/create-mimir-loki-managed-rule.md" >}})
- [View existing alerting rules and manage their current state]({{< relref "alerting-rules/rule-list.md" >}})
- [View the state and health of alerting rules]({{< relref "./fundamentals/state-and-health.md" >}})
- [Add or edit an alert contact point]({{< relref "./contact-points.md" >}})
@@ -12,9 +12,9 @@ While queries and expressions select the data set to evaluate, a condition sets
You can:
- [Create Cortex or Loki managed alert rule]({{< relref "./create-cortex-loki-managed-rule.md" >}})
- [Create Cortex or Loki managed recording rule]({{< relref "./create-cortex-loki-managed-recording-rule.md" >}})
- [Edit Cortex or Loki rule groups and namespaces]({{< relref "./edit-cortex-loki-namespace-group.md" >}})
- [Create Grafana Mimir or Loki managed alert rule]({{< relref "./create-mimir-loki-managed-rule.md" >}})
- [Create Grafana Mimir or Loki managed recording rule]({{< relref "./create-mimir-loki-managed-recording-rule.md" >}})
- [Edit Grafana Mimir or Loki rule groups and namespaces]({{< relref "./edit-mimir-loki-namespace-group.md" >}})
- [Create Grafana managed alert rule]({{< relref "./create-grafana-managed-rule.md" >}})
- [State and health of alerting rules]({{< relref "../fundamentals/state-and-health.md" >}})
- [Manage alerting rules]({{< relref "./rule-list.md" >}})
@@ -1,38 +1,38 @@
+++
title = "Create Cortex or Loki managed recording rule"
description = "Create Cortex or Loki managed recording rule"
title = "Create Grafana Mimir or Loki managed recording rule"
description = "Create Grafana Mimir or Loki managed recording rule"
keywords = ["grafana", "alerting", "guide", "rules", "recording rules", "create"]
weight = 400
+++
# Create a Cortex or Loki managed recording rule
# Create a Grafana Mimir or Loki managed recording rule
You can create and manage recording rules for an external Cortex or Loki instance. Recording rules calculate frequently needed expressions or computationally expensive expressions in advance and save the result as a new set of time series. Querying this new time series is faster, especially for dashboards since they query the same expression every time the dashboards refresh.
You can create and manage recording rules for an external Grafana Mimir or Loki instance. Recording rules calculate frequently needed expressions or computationally expensive expressions in advance and save the result as a new set of time series. Querying this new time series is faster, especially for dashboards since they query the same expression every time the dashboards refresh.
## Before you begin
For Cortex and Loki data sources to work with Grafana 8.0 alerting, enable the ruler API by configuring their respective services.
For Grafana Mimir and Loki data sources to work with Grafana 8.0 alerting, enable the ruler API by configuring their respective services.
**Loki** - The `local` rule storage type, default for the Loki data source, supports only viewing of rules. To edit rules, configure one of the other rule storage types.
**Cortex** - When configuring a Grafana Prometheus data source to point to Cortex, use the legacy `/api/prom` prefix, not `/prometheus`. Currently, we support only single-binary mode and you cannot provide a separate URL for the ruler API.
**Grafana Mimir** - When configuring a Grafana Prometheus data source to point to Grafana Mimir, use the legacy `/api/prom` prefix, not `/prometheus`. Currently, we support only single-binary mode and you cannot provide a separate URL for the ruler API.
> **Note:** If you do not want to manage alerting rules for a particular Loki or Prometheus data source, go to its settings page and clear the **Manage alerts via Alerting UI** checkbox.
## Add a Cortex or Loki managed recording rule
## Add a Grafana Mimir or Loki managed recording rule
1. In the Grafana menu, click the **Alerting** (bell) icon to open the Alerting page listing existing alerts.
1. Click **New alert rule**.
1. In Step 1, add the rule name, type, and storage location.
- In **Rule name**, add a descriptive name. This name is displayed in the alert rule list. It is also the `alertname` label for every alert instance that is created from this rule.
- From the **Rule type** drop-down, select **Cortex / Loki managed alert**.
- From the **Rule type** drop-down, select **Mimir / Loki managed alert**.
- From the **Select data source** drop-down, select an external Prometheus, an external Loki, or a Grafana Cloud data source.
- From the **Namespace** drop-down, select an existing rule namespace. Otherwise, click **Add new** and enter a name to create a new one. Namespaces can contain one or more rule groups and only have an organizational purpose.
- From the **Group** drop-down, select an existing group within the selected namespace. Otherwise, click **Add new** and enter a name to create a new one. Newly created rules are appended to the end of the group. Rules within a group are run sequentially at a regular interval, with the same evaluation time.
{{< figure src="/static/img/docs/alerting/unified/rule-edit-cortex-alert-type-8-0.png" max-width="550px" caption="Alert details" >}}
{{< figure src="/static/img/docs/alerting/unified/rule-edit-mimir-alert-type-8-0.png" max-width="550px" caption="Alert details" >}}
1. In Step 2, add the query to evaluate.
- Enter a PromQL or LogQL expression. The rule fires if the evaluation result has at least one series with a value that is greater than 0. An alert is created for each series.
{{< figure src="/static/img/docs/alerting/unified/rule-edit-cortex-query-8-0.png" max-width="550px" caption="Alert details" >}}
{{< figure src="/static/img/docs/alerting/unified/rule-edit-mimir-query-8-0.png" max-width="550px" caption="Alert details" >}}
1. In Step 3, add additional metadata associated with the rule.
- Add a description and summary to customize alert messages. Use the guidelines in [Annotations and labels for alerting]({{< relref "./alert-annotation-label.md" >}}).
- Add Runbook URL, panel, dashboard, and alert IDs.
@@ -1,40 +1,40 @@
+++
title = "Create Cortex or Loki managed alert rule"
description = "Create Cortex or Loki managed alerting rule"
title = "Create Grafana Mimir or Loki managed alert rule"
description = "Create Grafana Mimir or Loki managed alerting rule"
keywords = ["grafana", "alerting", "guide", "rules", "create"]
weight = 400
+++
# Create a Cortex or Loki managed alerting rule
# Create a Grafana Mimir or Loki managed alerting rule
Grafana allows you to create alerting rules for an external Cortex or Loki instance.
Grafana allows you to create alerting rules for an external Grafana Mimir or Loki instance.
## Before you begin
- Verify that you have write permission to the Prometheus data source. Otherwise, you will not be able to create or update Cortex managed alerting rules.
- Verify that you have write permission to the Prometheus data source. Otherwise, you will not be able to create or update Grafana Mimir managed alerting rules.
- For Cortex and Loki data sources, enable the ruler API by configuring their respective services.
- For Grafana Mimir and Loki data sources, enable the ruler API by configuring their respective services.
- **Loki** - The `local` rule storage type, default for the Loki data source, supports only viewing of rules. To edit rules, configure one of the other rule storage types.
- **Cortex** - use the [legacy `/api/prom` prefix](https://cortexmetrics.io/docs/api/#path-prefixes), not `/prometheus`. The Prometheus data source supports both Cortex and Prometheus, and Grafana expects that both the [Query API](https://cortexmetrics.io/docs/api/#querier--query-frontend) and [Ruler API](https://cortexmetrics.io/docs/api/#ruler) are under the same URL. You cannot provide a separate URL for the Ruler API.
- **Grafana Mimir** - use the [legacy `/api/prom` prefix](https://grafana.com/docs/mimir/latest/operators-guide/reference-http-api/#path-prefixes), not `/prometheus`. The Prometheus data source supports both Grafana Mimir and Prometheus, and Grafana expects that both the [Query API](https://grafana.com/docs/mimir/latest/operators-guide/reference-http-api/#querier--query-frontend) and [Ruler API](https://grafana.com/docs/mimir/latest/operators-guide/reference-http-api/#ruler) are under the same URL. You cannot provide a separate URL for the Ruler API.
> **Note:** If you do not want to manage alerting rules for a particular Loki or Prometheus data source, go to its settings and clear the **Manage alerts via Alerting UI** checkbox.
## Add a Cortex or Loki managed alerting rule
## Add a Grafana Mimir or Loki managed alerting rule
1. In the Grafana menu, click the **Alerting** (bell) icon to open the Alerting page listing existing alerts.
1. Click **New alert rule**.
1. In Step 1, add the rule name, type, and storage location.
- In **Rule name**, add a descriptive name. This name is displayed in the alert rule list. It is also the `alertname` label for every alert instance that is created from this rule.
- From the **Rule type** drop-down, select **Cortex / Loki managed alert**.
- From the **Rule type** drop-down, select **Mimir / Loki managed alert**.
- From the **Select data source** drop-down, select an external Prometheus, an external Loki, or a Grafana Cloud data source.
- From the **Namespace** drop-down, select an existing rule namespace. Otherwise, click **Add new** and enter a name to create a new one. Namespaces can contain one or more rule groups and only have an organizational purpose. For more information, see [Cortex or Loki rule groups and namespaces]({{< relref "./edit-cortex-loki-namespace-group.md" >}}).
- From the **Namespace** drop-down, select an existing rule namespace. Otherwise, click **Add new** and enter a name to create a new one. Namespaces can contain one or more rule groups and only have an organizational purpose. For more information, see [Grafana Mimir or Loki rule groups and namespaces]({{< relref "./edit-mimir-loki-namespace-group.md" >}}).
- From the **Group** drop-down, select an existing group within the selected namespace. Otherwise, click **Add new** and enter a name to create a new one. Newly created rules are appended to the end of the group. Rules within a group are run sequentially at a regular interval, with the same evaluation time.
{{< figure src="/static/img/docs/alerting/unified/rule-edit-cortex-alert-type-8-0.png" max-width="550px" caption="Alert details" >}}
{{< figure src="/static/img/docs/alerting/unified/rule-edit-mimir-alert-type-8-0.png" max-width="550px" caption="Alert details" >}}
1. In Step 2, add the query to evaluate.
- Enter a PromQL or LogQL expression. The rule fires if the evaluation result has at least one series with a value that is greater than 0. An alert is created for each series.
{{< figure src="/static/img/docs/alerting/unified/rule-edit-cortex-query-8-0.png" max-width="550px" caption="Alert details" >}}
{{< figure src="/static/img/docs/alerting/unified/rule-edit-mimir-query-8-0.png" max-width="550px" caption="Alert details" >}}
1. In Step 3, add conditions.
- In the **For** text box, specify the duration for which the condition must be true before an alert fires. If you specify `5m`, the condition must be true for 5 minutes before the alert fires.
> **Note:** Once a condition is met, the alert goes into the `Pending` state. If the condition remains active for the duration specified, the alert transitions to the `Firing` state, else it reverts to the `Normal` state.
@@ -1,24 +1,24 @@
+++
title = "Cortex or Loki rule groups and namespaces"
description = "Edit Cortex or Loki rule groups and namespaces"
keywords = ["grafana", "alerting", "guide", "group", "namespace", "cortex", "loki"]
title = "Grafana Mimir or Loki rule groups and namespaces"
description = "Edit Grafana Mimir or Loki rule groups and namespaces"
keywords = ["grafana", "alerting", "guide", "group", "namespace", "grafana mimir", "loki"]
weight = 405
+++
# Cortex or Loki rule groups and namespaces
# Grafana Mimir or Loki rule groups and namespaces
A namespace contains one or more groups. The rules within a group are run sequentially at a regular interval. The default interval is one (1) minute. You can rename Cortex or Loki rule namespaces and groups, and edit group evaluation intervals.
A namespace contains one or more groups. The rules within a group are run sequentially at a regular interval. The default interval is one (1) minute. You can rename Grafana Mimir or Loki rule namespaces and groups, and edit group evaluation intervals.
![Group list](/static/img/docs/alerting/unified/rule-list-edit-cortex-loki-icon-8-2.png 'Rule group list screenshot')
![Group list](/static/img/docs/alerting/unified/rule-list-edit-mimir-loki-icon-8-2.png 'Rule group list screenshot')
{{< figure src="/static/img/docs/alerting/unified/rule-list-edit-cortex-loki-icon-8-2.png" max-width="550px" caption="Alert details" >}}
{{< figure src="/static/img/docs/alerting/unified/rule-list-edit-mimir-loki-icon-8-2.png" max-width="550px" caption="Alert details" >}}
## Rename a namespace
To rename a namespace:
1. In the Grafana menu, click the **Alerting** (bell) icon to open the Alerting page listing existing alerts.
1. Find a Cortex or Loki managed rule with the group that belongs to the namespace you want to edit.
1. Find a Grafana Mimir or Loki managed rule with the group that belongs to the namespace you want to edit.
1. Click the **Edit** (pen) icon.
1. Enter a new name in the **Namespace** field, then click **Save changes**.
@@ -29,11 +29,11 @@ A new namespace is created and all groups are copied into this namespace from th
The rules within a group are run sequentially at a regular interval, the default interval is one (1) minute. You can modify this interval using the following instructions.
1. n the Grafana menu, click the **Alerting** (bell) icon to open the Alerting page listing existing alerts.
1. Find a Cortex or Loki managed rule with the group you want to edit.
1. Find a Grafana Mimir or Loki managed rule with the group you want to edit.
1. Click **Edit** (pen) icon.
1. Modify the **Rule group** and **Rule group evaluation interval** information as necessary.
1. Click **Save changes**.
When you rename the group, a new group with all the rules from the old group is created. The old group is deleted.
![Group edit modal](/static/img/docs/alerting/unified/rule-list-cortex-loki-edit-ns-group-8-2.png 'Rule group edit modal screenshot')
![Group edit modal](/static/img/docs/alerting/unified/rule-list-mimir-loki-edit-ns-group-8-2.png 'Rule group edit modal screenshot')
@@ -9,11 +9,14 @@ weight = 402
The Alerting page lists existing Grafana 8 alerting rules. By default, rules are grouped by types of data sources. The Grafana section lists all Grafana managed rules. Alerting rules for Prometheus compatible data sources are also listed here. You can view alerting rules for Prometheus compatible data sources but you cannot edit them.
The Cortex/Loki rules section lists all rules for external Prometheus or Loki data sources. Cloud alerting rules are also listed in this section.
The Mimir/Loki rules section lists all rules for external Prometheus or Loki data sources. Cloud alerting rules are also listed in this section.
- [View alerting rules](#view-alerting-rule)
- [Filter alerting rules](#filter-alerting-rules)
- [Edit or delete an alerting rule](#edit-or-delete-an-alerting-rule)
- [Manage alerting rules](#manage-alerting-rules)
- [View alerting rules](#view-alerting-rules)
- [Group view](#group-view)
- [State view](#state-view)
- [Filter alerting rules](#filter-alerting-rules)
- [Edit or delete an alerting rule](#edit-or-delete-an-alerting-rule)
## View alerting rules
@@ -47,9 +50,9 @@ To filter alerting rules:
## Edit or delete an alerting rule
Grafana managed alerting rules can only be edited or deleted by users with Edit permissions for the folder storing the rules. Alerting rules for an external Cortex or Loki instance can be edited or deleted by users with Editor or Admin roles.
Grafana managed alerting rules can only be edited or deleted by users with Edit permissions for the folder storing the rules. Alerting rules for an external Grafana Mimir or Loki instance can be edited or deleted by users with Editor or Admin roles.
To edit or delete a rule:
1. Expand a rule row until you can see the rule controls of **View**, **Edit**, and **Delete**.
1. Click **Edit** to open the create rule page. Make updates following instructions in [Create a Grafana managed alerting rule]({{< relref "./create-grafana-managed-rule.md" >}}) or [Create a Cortex or Loki managed alerting rule]({{< relref "./create-cortex-loki-managed-rule.md" >}}).
1. Click **Edit** to open the create rule page. Make updates following instructions in [Create a Grafana managed alerting rule]({{< relref "./create-grafana-managed-rule.md" >}}) or [Create a Grafana Mimir or Loki managed alerting rule]({{< relref "./create-mimir-loki-managed-rule.md" >}}).
1. Click **Delete** to delete a rule.
@@ -19,9 +19,9 @@ Unlike legacy dashboard alerts, Grafana alerts allow you to create queries and e
Since unified alerts are no longer directly tied to panel queries, they do not include images or query values in the notification email. You can use customized notification templates to view query values.
## Create Loki and Cortex alerting rules
## Create Loki and Grafana Mimir alerting rules
In Grafana alerting, you can manage Loki and Cortex alerting rules using the same UI and API as your Grafana managed alerts.
In Grafana alerting, you can manage Loki and Grafana Mimir alerting rules using the same UI and API as your Grafana managed alerts.
## View and search for alerts from Prometheus compatible data sources
@@ -12,7 +12,7 @@ Grafana includes built-in support for Prometheus Alertmanager. By default, notif
> **Note:** Before v8.2, the configuration of the embedded Alertmanager was shared across organizations. If you are on an older Grafana version, we recommend that you use Grafana alerts only if you have one organization. Otherwise, your contact points are visible to all organizations.
Grafana alerting added support for external Alertmanager configuration. When you add an [Alertmanager data source]({{< relref "../../../datasources/alertmanager.md" >}}), the Alertmanager drop-down shows a list of available external Alertmanager data sources. Select a data source to create and manage alerting for standalone Cortex or Loki data sources.
Grafana alerting added support for external Alertmanager configuration. When you add an [Alertmanager data source]({{< relref "../../../datasources/alertmanager.md" >}}), the Alertmanager drop-down shows a list of available external Alertmanager data sources. Select a data source to create and manage alerting for standalone Grafana Mimir or Loki data sources.
{{< figure max-width="40%" src="/static/img/docs/alerting/unified/contact-points-select-am-8-0.gif" max-width="250px" caption="Select Alertmanager" >}}
+1 -2
View File
@@ -12,7 +12,7 @@ Grafana includes built-in support for Prometheus Alertmanager. It is presently i
## Alertmanager implementations
[Prometheus](https://prometheus.io/) and [Cortex](https://cortexmetrics.io/) (default) implementations of Alertmanager are supported. You can specify implementation in the data source settings page. In case of Prometheus contact points and notification policies are read-only in the Grafana alerting UI, as it does not support updating configuration via HTTP API.
[Prometheus](https://prometheus.io/) and [Grafana Mimir](https://grafana.com/docs/mimir/latest/) (default) implementations of Alertmanager are supported. You can specify implementation in the data source settings page. In case of Prometheus contact points and notification policies are read-only in the Grafana alerting UI, as it does not support updating configuration via HTTP API.
## Provision the Alertmanager data source
@@ -29,7 +29,6 @@ datasources:
url: http://localhost:9093
access: proxy
jsonData:
implementation: 'prometheus' # alternatively 'cortex'
# optionally
basicAuth: true
basicAuthUser: my_user
+9 -9
View File
@@ -28,15 +28,15 @@ To access Jaeger settings, click the **Configuration** (gear) icon, then click *
> **Note:** This feature is available in Grafana 7.4+.
This is a configuration for the [trace to logs feature]({{< relref "../explore/trace-integration" >}}). Select target data source (at this moment limited to Loki data sources) and select which tags will be used in the logs query.
This is a configuration for the [trace to logs feature]({{< relref "../explore/trace-integration" >}}). Select target data source (at this moment limited to Loki and Splunk \[logs\] data sources) and select which tags will be used in the logs query.
- **Data source -** Target data source.
- **Tags -** The tags that will be used in the Loki query. Default is `'cluster', 'hostname', 'namespace', 'pod'`.
- **Map tag names -** When enabled, allows configuring how Jaeger tag names map to Loki label names. For example, map `service.name` to `service`.
- **Span start time shift -** Shift in the start time for the Loki query based on the span start time. In order to extend to the past, you need to use a negative value. Use time interval units like 5s, 1m, 3h. The default is 0.
- **Span end time shift -** Shift in the end time for the Loki query based on the span end time. Time units can be used here, for example, 5s, 1m, 3h. The default is 0.
- **Filter by Trace ID -** Toggle to append the trace ID to the Loki query.
- **Filter by Span ID -** Toggle to append the span ID to the Loki query.
- **Tags -** The tags that will be used in the logs query. Default is `'cluster', 'hostname', 'namespace', 'pod'`.
- **Map tag names -** When enabled, allows configuring how Jaeger tag names map to logs label names. For example, map `service.name` to `service`.
- **Span start time shift -** Shift in the start time for the logs query based on the span start time. In order to extend to the past, you need to use a negative value. Use time interval units like 5s, 1m, 3h. The default is 0.
- **Span end time shift -** Shift in the end time for the logs query based on the span end time. Time units can be used here, for example, 5s, 1m, 3h. The default is 0.
- **Filter by Trace ID -** Toggle to append the trace ID to the logs query.
- **Filter by Span ID -** Toggle to append the span ID to the logs query.
![Trace to logs settings](/static/img/docs/explore/trace-to-logs-settings-8-2.png 'Screenshot of the trace to logs settings')
@@ -146,8 +146,8 @@ datasources:
isDefault: false
jsonData:
tracesToLogs:
# Field with internal link pointing to a Loki data source in Grafana.
# datasourceUid value must match the `datasourceUid` value of the Loki data source.
# Field with internal link pointing to a logs data source in Grafana.
# datasourceUid value must match the `datasourceUid` value of the logs data source.
datasourceUid: 'loki'
tags: ['job', 'instance', 'pod', 'namespace']
mappedTags: [{ key: 'service.name', value: 'service' }]
+1 -1
View File
@@ -30,7 +30,7 @@ To access data source settings, hover your mouse over the **Configuration** (gea
### Min time interval
A lower limit for the [$__interval]({{< relref "../variables/variable-types/_index.md#the-interval-variable" >}}) and [$__interval_ms]({{< relref "../variables/variable-types/_index.md#the-interval-ms-variable" >}}) variables.
A lower limit for the [$__interval]({{< relref "../variables/variable-types/global-variables/#__interval" >}}) and [$__interval_ms]({{< relref "../variables/variable-types/global-variables/#__interval_ms" >}}) variables.
Recommended to be set to write frequency, for example `1m` if your data is written every minute.
This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formatted as a
number followed by a valid time identifier, e.g. `1m` (1 minute) or `30s` (30 seconds). The following time identifiers are supported:
+1 -1
View File
@@ -36,7 +36,7 @@ Grafana ships with a built-in MySQL data source plugin that allows you to query
### Min time interval
A lower limit for the [$__interval]({{< relref "../variables/variable-types/_index.md#the-interval-variable" >}}) and [$__interval_ms]({{< relref "../variables/variable-types/_index.md#the-interval-ms-variable" >}}) variables.
A lower limit for the [$__interval]({{< relref "../variables/variable-types/global-variables/#__interval" >}}) and [$__interval_ms]({{< relref "../variables/variable-types/global-variables/#__interval_ms" >}}) variables.
Recommended to be set to write frequency, for example `1m` if your data is written every minute.
This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formatted as a
number followed by a valid time identifier, e.g. `1m` (1 minute) or `30s` (30 seconds). The following time identifiers are supported:
+1 -1
View File
@@ -33,7 +33,7 @@ To access PostgreSQL settings, hover your mouse over the **Configuration** (gear
### Min time interval
A lower limit for the [$__interval]({{< relref "../variables/variable-types/_index.md#the-interval-variable" >}}) and [$__interval_ms]({{< relref "../variables/variable-types/_index.md#the-interval-ms-variable" >}}) variables.
A lower limit for the [$__interval]({{< relref "../variables/variable-types/global-variables/#__interval" >}}) and [$__interval_ms]({{< relref "../variables/variable-types/global-variables/#__interval_ms" >}}) variables.
Recommended to be set to write frequency, for example `1m` if your data is written every minute.
This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formatted as a
number followed by a valid time identifier, e.g. `1m` (1 minute) or `30s` (30 seconds). The following time identifiers are supported:
+1 -1
View File
@@ -200,7 +200,7 @@ For detailed instructions, refer to [Internal Grafana metrics]({{< relref "../ad
The Prometheus data source works with other projects that implement the [Prometheus query API](https://prometheus.io/docs/prometheus/latest/querying/api/) including:
- [Cortex](https://cortexmetrics.io/docs/)
- [Grafana Mimir](https://grafana.com/docs/mimir/latest/)
- [Thanos](https://thanos.io/v0.17/components/query.md/)
For more information on how to query other Prometheus-compatible projects from Grafana, refer to the specific project documentation.
+7 -7
View File
@@ -27,15 +27,15 @@ To access Tempo settings, click the **Configuration** (gear) icon, then click **
> **Note:** This feature is available in Grafana 7.4+.
This is a configuration for the [trace to logs feature]({{< relref "../explore/trace-integration" >}}). Select target data source (at this moment limited to Loki data sources) and select which tags will be used in the logs query.
This is a configuration for the [trace to logs feature]({{< relref "../explore/trace-integration" >}}). Select target data source (at this moment limited to Loki or Splunk \[logs\] data sources) and select which tags will be used in the logs query.
- **Data source -** Target data source.
- **Tags -** The tags that will be used in the Loki query. Default is `'cluster', 'hostname', 'namespace', 'pod'`.
- **Map tag names -** When enabled, allows configuring how Tempo tag names map to Loki label names. For example, map `service.name` to `service`.
- **Span start time shift -** A shift in the start time for the Loki query based on the start time for the span. To extend the time to the past, use a negative value. You can use time units, for example, 5s, 1m, 3h. The default is 0.
- **Span end time shift -** Shift in the end time for the Loki query based on the span end time. Time units can be used here, for example, 5s, 1m, 3h. The default is 0.
- **Filter by Trace ID -** Toggle to append the trace ID to the Loki query.
- **Filter by Span ID -** Toggle to append the span ID to the Loki query.
- **Tags -** The tags that will be used in the logs query. Default is `'cluster', 'hostname', 'namespace', 'pod'`.
- **Map tag names -** When enabled, allows configuring how Tempo tag names map to logs label names. For example, map `service.name` to `service`.
- **Span start time shift -** A shift in the start time for the logs query based on the start time for the span. To extend the time to the past, use a negative value. You can use time units, for example, 5s, 1m, 3h. The default is 0.
- **Span end time shift -** Shift in the end time for the logs query based on the span end time. Time units can be used here, for example, 5s, 1m, 3h. The default is 0.
- **Filter by Trace ID -** Toggle to append the trace ID to the logs query.
- **Filter by Span ID -** Toggle to append the span ID to the logs query.
{{< figure src="/static/img/docs/explore/traces-to-logs-settings-8-2.png" class="docs-image--no-shadow" caption="Screenshot of the trace to logs settings" >}}
+8 -8
View File
@@ -28,15 +28,15 @@ To access Zipkin settings, click the **Configuration** (gear) icon, then click *
> **Note:** This feature is available in Grafana 7.4+.
This is a configuration for the [trace to logs feature]({{< relref "../explore/trace-integration" >}}). Select target data source (at this moment limited to Loki data sources) and select which tags will be used in the logs query.
This is a configuration for the [trace to logs feature]({{< relref "../explore/trace-integration" >}}). Select target data source (at this moment limited to Loki or Splunk \[logs\] data sources) and select which tags will be used in the logs query.
- **Data source -** Target data source.
- **Tags -** The tags that will be used in the Loki query. Default is `'cluster', 'hostname', 'namespace', 'pod'`.
- **Map tag names -** When enabled, allows configuring how Zipkin tag names map to Loki label names. For example, map `service.name` to `service`.
- **Span start time shift -** Shift in the start time for the Loki query based on the span start time. In order to extend to the past, you need to use a negative value. Use time interval units like 5s, 1m, 3h. The default is 0.
- **Span end time shift -** Shift in the end time for the Loki query based on the span end time. Time units can be used here, for example, 5s, 1m, 3h. The default is 0.
- **Filter by Trace ID -** Toggle to append the trace ID to the Loki query.
- **Filter by Span ID -** Toggle to append the span ID to the Loki query.
- **Tags -** The tags that will be used in the logs query. Default is `'cluster', 'hostname', 'namespace', 'pod'`.
- **Map tag names -** When enabled, allows configuring how Zipkin tag names map to logs label names. For example, map `service.name` to `service`.
- **Span start time shift -** Shift in the start time for the logs query based on the span start time. In order to extend to the past, you need to use a negative value. Use time interval units like 5s, 1m, 3h. The default is 0.
- **Span end time shift -** Shift in the end time for the logs query based on the span end time. Time units can be used here, for example, 5s, 1m, 3h. The default is 0.
- **Filter by Trace ID -** Toggle to append the trace ID to the logs query.
- **Filter by Span ID -** Toggle to append the span ID to the logs query.
![Trace to logs settings](/static/img/docs/explore/trace-to-logs-settings-8-2.png 'Screenshot of the trace to logs settings')
@@ -98,4 +98,4 @@ Here is an example JSON:
## Linking Trace ID from logs
You can link to Zipkin trace from logs in Loki by configuring a derived field with internal link. See [Loki documentation]({{< relref "loki#derived-fields" >}}) for details.
You can link to Zipkin trace from logs in Loki or Splunk by configuring a derived field with internal link. See [Loki documentation]({{< relref "loki#derived-fields" >}}) for details.
@@ -315,4 +315,15 @@ func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataReque
}
```
The `Authorization` and `X-ID-Token` headers will also be available on the `CallResourceRequest` object on the `CallResource` request in your backend data source when `jsonData.oauthPassThru` is `true`.
```go
func (ds *dataSource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
token := req.Headers["Authorization"]
idToken := req.Headers["X-ID-Token"] // present if user's token includes an ID token
// ...
}
```
> **Note:** Due to a bug in Grafana, using this feature with PostgreSQL can cause a deadlock. For more information, refer to [Grafana causes deadlocks in PostgreSQL, while trying to refresh users token](https://github.com/grafana/grafana/issues/20515).
@@ -10,3 +10,7 @@ weight = 800
This panel visualization displays an RSS feed. By default, it displays articles from the Grafana Labs blog.
Enter the URL of an RSS in the URL field in the Display section. This panel type does not accept any other queries.
In version 8.5, we discontinued the "Use Proxy" option for Grafana news panels. As a result, RSS feeds that are not configured for request by Grafana's frontend (with the appropriate [CORS headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)) may not load.
If the RSS feed you're trying to display fails to load, consider rehosting the RSS feed or prefixing the RSS URL with your own "CORS proxy". Alternatively, you can use the community [RSS/Atom data source](https://grafana.com/grafana/plugins/volkovlabs-rss-datasource/) in combination with the [Dynamic text](https://grafana.com/grafana/plugins/marcusolsson-dynamictext-panel/) community panel to display the RSS feed.
+14 -9
View File
@@ -52,7 +52,7 @@ require (
github.com/gosimple/slug v1.9.0
github.com/grafana/cuetsy v0.0.0-20211119211437-8c25464cc9bf
github.com/grafana/grafana-aws-sdk v0.10.1
github.com/grafana/grafana-azure-sdk-go v1.0.0
github.com/grafana/grafana-azure-sdk-go v1.1.0
github.com/grafana/grafana-plugin-sdk-go v0.129.0
github.com/grafana/loki v1.6.2-0.20211015002020-7832783b1caa
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
@@ -127,7 +127,7 @@ require (
require (
cloud.google.com/go v0.97.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.1 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
@@ -174,7 +174,7 @@ require (
github.com/go-openapi/validate v0.20.2 // indirect
github.com/gogo/googleapis v1.4.1 // indirect
github.com/gogo/status v1.1.0 // indirect
github.com/golang-jwt/jwt/v4 v4.1.0 // indirect
github.com/golang-jwt/jwt/v4 v4.2.0 // indirect
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
@@ -182,7 +182,7 @@ require (
github.com/gomodule/redigo v2.0.0+incompatible // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/flatbuffers v2.0.0+incompatible // indirect
github.com/googleapis/gax-go/v2 v2.1.1 // indirect
github.com/googleapis/gax-go/v2 v2.1.1
github.com/gorilla/mux v1.8.0 // indirect
github.com/grafana/grafana-google-sdk-go v0.0.0-20211104130251-b190293eaf58
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect
@@ -246,20 +246,24 @@ require (
golang.org/x/text v0.3.7 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1 // indirect
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect
)
require (
cloud.google.com/go/kms v1.1.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.13.2
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.4.0
github.com/Azure/go-autorest/autorest/adal v0.9.17
github.com/golang-migrate/migrate/v4 v4.7.0
github.com/grafana/dskit v0.0.0-20211011144203-3a88ec0b675f
gocloud.dev v0.24.0
)
require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.10.0 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.17 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.22.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.2.1 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0 // indirect
github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
github.com/chromedp/cdproto v0.0.0-20220208224320-6efb837e6bc2 // indirect
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect
@@ -270,9 +274,10 @@ require (
github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect
github.com/getkin/kin-openapi v0.91.0 // indirect
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 // indirect
github.com/grafana/dskit v0.0.0-20211011144203-3a88ec0b675f // indirect
github.com/golang-jwt/jwt v3.2.1+incompatible // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/klauspost/compress v1.13.6 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/pierrec/lz4/v4 v4.1.8 // indirect
github.com/segmentio/asm v1.1.1 // indirect
+30 -10
View File
@@ -106,12 +106,21 @@ github.com/Azure/azure-sdk-for-go v55.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9mo
github.com/Azure/azure-sdk-for-go v57.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-sdk-for-go v59.3.0+incompatible h1:dPIm0BO4jsMXFcCI/sLTPkBtE7mk8WMuRHA0JeWhlcQ=
github.com/Azure/azure-sdk-for-go v59.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0 h1:lhSJz9RMbJcTgxifR1hUNJnn6CNYtbgEDtQV22/9RBA=
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.10.0 h1:jq5Urf8QJK6h0wr8CMiwggo4OSMkXwpArQlkSjSpaBk=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.10.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0 h1:v9p9TfTbf7AwNb5NYQt7hI41IfPoLFiFkLtb+bmGjT0=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.20.0/go.mod h1:ZPW/Z0kLCTdDZaDbYTetxc9Cxl/2lNqxYHYNOF2bti0=
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.0/go.mod h1:fBF9PQNqB8scdgpZ3ufzaLntG0AG7C1WjPMsiFOmfHM=
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.22.0 h1:zBJcBJwte0x6PcPK7XaWDMvK2o2ZM2f1sMaqNNavQ5g=
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.22.0/go.mod h1:fBF9PQNqB8scdgpZ3ufzaLntG0AG7C1WjPMsiFOmfHM=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.13.0/go.mod h1:TmXReXZ9yPp5D5TBRMTAtyz+UyOl15Py4hL5E5p6igQ=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.13.2 h1:mM/yraAumqMMIYev6zX0oxHqX6hreUs5wXf76W47r38=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.13.2/go.mod h1:+nVKciyKD2J9TyVcEQ82Bo9b+3F92PiQfHrIE/zqLqM=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.1/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.3/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.1 h1:sLZ/Y+P/5RRtsXWylBjB5lkgixYfm0MQPiwrSX//JSo=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.1/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I=
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.4.0 h1:t10+CFWGK92HGTQaYZyXchiVetuEWfND3abV8inz6n8=
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.4.0/go.mod h1:LxYa4KH5ni+OMT8DJBAP6FVYg3YFW3ACTJluqpnTDBg=
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.2.1 h1:lirjIOHv5RrmDbZXw9lUz/fY68uU05qR4uIef58WMvQ=
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.2.1/go.mod h1:j1J9XXIo/eXD7YSrr73sYZTEY/AQ0+/Q6Aa96z1e2j8=
github.com/Azure/azure-service-bus-go v0.11.5/go.mod h1:MI6ge2CuQWBVq+ly456MY7XqNLJip5LO1iSFodbNLbU=
github.com/Azure/azure-storage-blob-go v0.6.0/go.mod h1:oGfmITT1V6x//CswqY2gtAHND+xIP64/qL7a5QJix0Y=
github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0=
@@ -188,6 +197,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZ
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0 h1:WVsrXCnHlDDX8ls+tootqRE87/hL9S/g4ewig9RsD/c=
github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
@@ -720,7 +731,8 @@ github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 h1:Izz0+t1Z5nI16/II7vuEo/nHjodOg0p7+OiDpjX5t1E=
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/dnaeon/go-vcr v1.1.0 h1:ReYa/UBrRyQdant9B4fNHGoCNKw6qh6P0fsdGmZpR7c=
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
github.com/docker/distribution v2.6.0-rc.1.0.20170726174610-edc3ab29cdff+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.7.0+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
@@ -1128,9 +1140,12 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69
github.com/gogo/status v1.0.3/go.mod h1:SavQ51ycCLnc7dGyJxp8YAmudx8xqiVrRf+6IXRsugc=
github.com/gogo/status v1.1.0 h1:+eIkrewn5q6b30y+g/BJINVVdi2xH7je5MPJ3ZPK3JA=
github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM=
github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c=
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.1.0 h1:XUgk2Ex5veyVFVeLm0xhusUTQybEbexJXrvPNOKkSY0=
github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-migrate/migrate/v4 v4.7.0 h1:gONcHxHApDTKXDyLH/H97gEHmpu1zcnnbAaq2zgrPrs=
github.com/golang-migrate/migrate/v4 v4.7.0/go.mod h1:Qvut3N4xKWjoH3sokBccML6WyHSnggXm/DvMMnTsQIc=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
@@ -1335,8 +1350,8 @@ github.com/grafana/go-mssqldb v0.0.0-20210326084033-d0ce3c521036 h1:GplhUk6Xes5J
github.com/grafana/go-mssqldb v0.0.0-20210326084033-d0ce3c521036/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/grafana/grafana-aws-sdk v0.10.1 h1:Ksguhjx6EuGLN/5Oc7oZoxuDReJ5RxIH99yqSMpLGUs=
github.com/grafana/grafana-aws-sdk v0.10.1/go.mod h1:vFIOHEnY1u5nY0/tge1IHQjPuG6DRKr2ISf/HikUdjE=
github.com/grafana/grafana-azure-sdk-go v1.0.0 h1:RIVQyVb89/y/BnOVsVDcxiMtmWF8NmAX8ql0OJvzwNc=
github.com/grafana/grafana-azure-sdk-go v1.0.0/go.mod h1:xbzMaG74BN4rOP1NYEsCMNWkPbK7GfSU09PGYfQYm+g=
github.com/grafana/grafana-azure-sdk-go v1.1.0 h1:Gh0fjs7jr4Lp5y+4cjn48U+MQaJeV8i9m1ds/1xszto=
github.com/grafana/grafana-azure-sdk-go v1.1.0/go.mod h1:rgrnK9m6CgKlgx4rH3FFP/6dTdyRO6LYC2mVZov35yo=
github.com/grafana/grafana-google-sdk-go v0.0.0-20211104130251-b190293eaf58 h1:2ud7NNM7LrGPO4x0NFR8qLq68CqI4SmB7I2yRN2w9oE=
github.com/grafana/grafana-google-sdk-go v0.0.0-20211104130251-b190293eaf58/go.mod h1:Vo2TKWfDVmNTELBUM+3lkrZvFtBws0qSZdXhQxRdJrE=
github.com/grafana/grafana-plugin-sdk-go v0.94.0/go.mod h1:3VXz4nCv6wH5SfgB3mlW39s+c+LetqSCjFj7xxPC5+M=
@@ -1894,6 +1909,7 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
@@ -2063,6 +2079,7 @@ github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuR
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
@@ -2823,6 +2840,7 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
@@ -2847,6 +2865,7 @@ golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 h1:DZshvxDdVoeKIbudAdFEKi+f70l51luSy/7b76ibTY0=
@@ -3044,6 +3063,7 @@ golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+8 -5
View File
@@ -32,7 +32,6 @@
"packages:publishLatest": "lerna publish from-package --contents dist --yes --no-verify-access",
"packages:publishNext": "lerna publish from-package --contents dist --dist-tag next --yes --no-verify-access",
"packages:publishTest": "lerna publish from-package --contents dist --dist-tag test --yes --no-verify-access",
"packages:publishPrevious": "lerna publish from-package --contents dist --dist-tag previous --yes --no-verify-access",
"packages:publishDev": "lerna publish from-package --contents dist --dist-tag dev --yes --registry http://grafana-npm.local:4873 --force-publish=*",
"packages:typecheck": "lerna run typecheck",
"packages:clean": "lerna run clean",
@@ -58,7 +57,7 @@
"betterer:stats": "ts-node --transpile-only --project ./scripts/cli/tsconfig.json ./scripts/cli/reportBettererStats.ts"
},
"grafana": {
"whatsNewUrl": "https://grafana.com/docs/grafana/next/whatsnew/whats-new-in-v8-4/",
"whatsNewUrl": "https://grafana.com/docs/grafana/next/whatsnew/whats-new-in-v8-5/",
"releaseNotesUrl": "https://grafana.com/docs/grafana/next/release-notes/"
},
"lint-staged": {
@@ -72,6 +71,9 @@
],
"*pkg/**/*.go": [
"gofmt -w -s"
],
"*.{star}": [
"make drone"
]
},
"devDependencies": {
@@ -329,7 +331,7 @@
"monaco-promql": "^1.7.2",
"mousetrap": "1.6.5",
"mousetrap-global-bind": "1.1.0",
"moveable": "0.27.3",
"moveable": "0.28.0",
"ol": "6.14.1",
"papaparse": "5.3.2",
"pluralize": "^8.0.0",
@@ -390,11 +392,12 @@
"resolutions": {
"underscore": "1.13.2",
"@types/slate": "0.47.2",
"@microsoft/api-extractor-model": "7.15.4",
"@microsoft/api-extractor-model": "7.16.0",
"@rushstack/node-core-library": "3.45.1",
"@rushstack/rig-package": "0.3.8",
"@rushstack/ts-command-line": "4.10.7",
"@storybook/react/webpack": "5.70.0"
"@storybook/react/webpack": "5.70.0",
"node-fetch": "2.6.7"
},
"workspaces": {
"packages": [
@@ -93,6 +93,7 @@ export type OAuthSettings = Partial<Record<OAuth, { name: string; icon?: string
export interface CurrentUserDTO {
isSignedIn: boolean;
id: number;
externalUserId: string;
login: string;
email: string;
name: string;
@@ -35,6 +35,8 @@ export interface DataSourcePickerProps {
variables?: boolean;
alerting?: boolean;
pluginId?: string;
/** If true,we show only DSs with logs; and if true, pluginId shouldnt be passed in */
logs?: boolean;
// If set to true and there is no value select will be empty, otherwise it will preselect default data source
noDefault?: boolean;
width?: number;
@@ -123,12 +125,15 @@ export class DataSourcePicker extends PureComponent<DataSourcePickerProps, DataS
}
getDataSourceOptions() {
const { alerting, tracing, metrics, mixed, dashboard, variables, annotations, pluginId, type, filter } = this.props;
const { alerting, tracing, metrics, mixed, dashboard, variables, annotations, pluginId, type, filter, logs } =
this.props;
const options = this.dataSourceSrv
.getList({
alerting,
tracing,
metrics,
logs,
dashboard,
mixed,
variables,
@@ -46,6 +46,9 @@ export interface GetDataSourceListFilters {
/** Only return data sources that support tracing response */
tracing?: boolean;
/** Only return data sources that support logging response */
logs?: boolean;
/** Only return data sources that support annotations */
annotations?: boolean;
+1 -1
View File
@@ -93,7 +93,7 @@
"sass": "1.27.0",
"sass-loader": "8.0.2",
"semver": "^7.1.3",
"simple-git": "^2.46.0",
"simple-git": "^3.5.0",
"style-loader": "1.1.3",
"terser-webpack-plugin": "2.3.7",
"ts-jest": "26.4.4",
@@ -1,5 +1,5 @@
import { Props, BigValueColorMode, BigValueGraphMode } from './BigValue';
import { buildLayout, StackedWithChartLayout, WideWithChartLayout } from './BigValueLayout';
import { Props, BigValueColorMode, BigValueGraphMode, BigValueTextMode } from './BigValue';
import { buildLayout, StackedWithChartLayout, StackedWithNoChartLayout, WideWithChartLayout } from './BigValueLayout';
import { ArrayVector, createTheme, FieldType } from '@grafana/data';
function getProps(propOverrides?: Partial<Props>): Props {
@@ -20,6 +20,7 @@ function getProps(propOverrides?: Partial<Props>): Props {
config: {},
},
},
count: 1,
theme: createTheme(),
};
@@ -39,6 +40,43 @@ describe('BigValueLayout', () => {
expect(layout).toBeInstanceOf(StackedWithChartLayout);
});
it('should not include title height when count is 1 and title is auto hidden', () => {
const layout = buildLayout(
getProps({
value: {
text: '25',
title: '10',
numeric: 25,
},
sparkline: undefined,
textMode: BigValueTextMode.Auto,
count: 1,
})
);
expect(layout.titleFontSize).toBe(0);
});
it('should not use chart layout if only one sparkline point', () => {
const layout = buildLayout(
getProps({
value: {
text: '25',
title: '10',
numeric: 25,
},
sparkline: {
y: {
name: '',
values: new ArrayVector([1]),
type: FieldType.number,
config: {},
},
},
})
);
expect(layout).toBeInstanceOf(StackedWithNoChartLayout);
});
it('should auto select to wide layout', () => {
const layout = buildLayout(
getProps({
@@ -38,8 +38,8 @@ export abstract class BigValueLayout {
this.justifyCenter = shouldJustifyCenter(props.justifyMode, this.textValues.title);
this.valueToAlignTo = this.textValues.valueToAlignTo;
this.titleToAlignTo = this.textValues.titleToAlignTo;
this.titleFontSize = 14;
this.valueFontSize = 14;
this.titleFontSize = 0;
this.valueFontSize = 0;
this.chartHeight = 0;
this.chartWidth = 0;
this.maxTextWidth = width - this.panelPadding * 2;
@@ -335,8 +335,9 @@ export class StackedWithChartLayout extends BigValueLayout {
LINE_HEIGHT,
MAX_TITLE_SIZE
);
titleHeight = this.titleFontSize * LINE_HEIGHT;
}
titleHeight = this.titleFontSize * LINE_HEIGHT;
if (this.valueToAlignTo.length) {
this.valueFontSize = calculateFontSize(
@@ -399,8 +400,10 @@ export class StackedWithNoChartLayout extends BigValueLayout {
);
}
// make title fontsize it's a bit smaller than valueFontSize
this.titleFontSize = Math.min(this.valueFontSize * 0.7, this.titleFontSize);
if (this.titleToAlignTo?.length) {
// make title fontsize it's a bit smaller than valueFontSize
this.titleFontSize = Math.min(this.valueFontSize * 0.7, this.titleFontSize);
}
}
getValueAndTitleContainerStyles() {
@@ -422,7 +425,7 @@ export function buildLayout(props: Props): BigValueLayout {
const useWideLayout = width / height > 2.5;
if (useWideLayout) {
if (height > 50 && !!sparkline) {
if (height > 50 && !!sparkline && sparkline.y.values.length > 1) {
return new WideWithChartLayout(props);
} else {
return new WideNoChartLayout(props);
@@ -430,7 +433,7 @@ export function buildLayout(props: Props): BigValueLayout {
}
// stacked layouts
if (height > 100 && !!sparkline) {
if (height > 100 && sparkline && sparkline.y.values.length > 1) {
return new StackedWithChartLayout(props);
} else {
return new StackedWithNoChartLayout(props);
@@ -87,7 +87,7 @@ export function RadioButtonGroup<T>({
>
{o.icon && <Icon name={o.icon as IconName} className={styles.icon} />}
{o.imgUrl && <img src={o.imgUrl} alt={o.label} className={styles.img} />}
{o.label}
{o.label} {o.component ? <o.component /> : null}
</RadioButton>
);
})}
@@ -2,7 +2,13 @@ import { XYFieldMatchers } from './types';
import { ArrayVector, DataFrame, FieldConfig, FieldType, outerJoinDataFrames, TimeRange } from '@grafana/data';
import { nullToUndefThreshold } from './nullToUndefThreshold';
import { applyNullInsertThreshold } from './nullInsertThreshold';
import { AxisPlacement, GraphFieldConfig, ScaleDistribution, ScaleDistributionConfig } from '@grafana/schema';
import {
AxisPlacement,
GraphDrawStyle,
GraphFieldConfig,
ScaleDistribution,
ScaleDistributionConfig,
} from '@grafana/schema';
import { FIXED_UNIT } from './GraphNG';
// will mutate the DataFrame's fields' values
@@ -31,7 +37,23 @@ function applySpanNullsThresholds(frame: DataFrame) {
export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers, timeRange?: TimeRange | null) {
let alignedFrame = outerJoinDataFrames({
frames: frames.map((frame) => applyNullInsertThreshold(frame, null, timeRange?.to.valueOf())),
frames: frames.map((frame) => {
let fr = applyNullInsertThreshold(frame, null, timeRange?.to.valueOf());
// prevent minesweeper-expansion of nulls (gaps) when joining bars
// since bar width is determined from the minimum distance between non-undefined values
// (this strategy will still retain any original pre-join nulls, though)
fr.fields.forEach((f) => {
if (f.type === FieldType.number && f.config.custom?.drawStyle === GraphDrawStyle.Bars) {
f.config.custom = {
...f.config.custom,
spanNulls: -1,
};
}
});
return fr;
}),
joinBy: dimFields.x,
keep: dimFields.y,
keepOriginIndices: true,
@@ -21,11 +21,25 @@ export interface Props {
children?: ReactNode;
className?: string;
isFullscreen?: boolean;
'aria-label'?: string;
}
/** @alpha */
export const PageToolbar: FC<Props> = React.memo(
({ title, parent, pageIcon, onGoBack, children, titleHref, parentHref, leftItems, isFullscreen, className }) => {
({
title,
parent,
pageIcon,
onGoBack,
children,
titleHref,
parentHref,
leftItems,
isFullscreen,
className,
/** main nav-container aria-label **/
'aria-label': ariaLabel,
}) => {
const styles = useStyles2(getStyles);
/**
@@ -44,7 +58,7 @@ export const PageToolbar: FC<Props> = React.memo(
);
return (
<div className={mainStyle}>
<nav className={mainStyle} aria-label={ariaLabel}>
{pageIcon && !onGoBack && (
<div className={styles.pageIcon}>
<Icon name={pageIcon} size="lg" aria-hidden />
@@ -110,7 +124,7 @@ export const PageToolbar: FC<Props> = React.memo(
</div>
);
})}
</div>
</nav>
);
}
);
@@ -45,13 +45,13 @@ export const VizTooltipContainer: React.FC<VizTooltipContainerProps> = ({
const tH = Math.floor(entry.contentRect.height + 2 * 8);
if (tooltipMeasurement.width !== tW || tooltipMeasurement.height !== tH) {
setTooltipMeasurement({
width: tW,
height: tH,
width: Math.min(tW, width),
height: Math.min(tH, height),
});
}
}
}),
[tooltipMeasurement]
[tooltipMeasurement, width, height]
);
useLayoutEffect(() => {
+4 -25
View File
@@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/login/loginservice"
"github.com/grafana/grafana/pkg/services/login/logintest"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting"
@@ -27,28 +28,6 @@ const (
existingTestLogin = "existing@example.com"
)
type mockAuthInfoService struct {
LatestUserID int64
ExpectedError error
}
func (m *mockAuthInfoService) LookupAndUpdate(ctx context.Context, query *models.GetUserByAuthInfoQuery) (*models.User, error) {
m.LatestUserID = query.UserId
return nil, m.ExpectedError
}
func (m *mockAuthInfoService) GetAuthInfo(ctx context.Context, query *models.GetAuthInfoQuery) error {
m.LatestUserID = query.UserId
return m.ExpectedError
}
func (m *mockAuthInfoService) SetAuthInfo(ctx context.Context, query *models.SetAuthInfoCommand) error {
return m.ExpectedError
}
func (m *mockAuthInfoService) UpdateAuthInfo(ctx context.Context, query *models.UpdateAuthInfoCommand) error {
return m.ExpectedError
}
func TestAdminAPIEndpoint(t *testing.T) {
const role = models.ROLE_ADMIN
@@ -282,7 +261,7 @@ func putAdminScenario(t *testing.T, desc string, url string, routePattern string
hs := &HTTPServer{
Cfg: setting.NewCfg(),
SQLStore: sqlStore,
authInfoService: &mockAuthInfoService{},
authInfoService: &logintest.AuthInfoServiceFake{},
}
sc := setupScenarioContext(t, url)
@@ -397,7 +376,7 @@ func adminDisableUserScenario(t *testing.T, desc string, action string, url stri
fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
authInfoService := &mockAuthInfoService{}
authInfoService := &logintest.AuthInfoServiceFake{}
hs := HTTPServer{
Bus: bus.GetBus(),
@@ -435,7 +414,7 @@ func adminDeleteUserScenario(t *testing.T, desc string, url string, routePattern
sc := setupScenarioContext(t, url)
sc.sqlStore = hs.SQLStore
sc.authInfoService = &mockAuthInfoService{}
sc.authInfoService = &logintest.AuthInfoServiceFake{}
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
sc.context = c
sc.context.UserId = testUserID
+1 -3
View File
@@ -4,7 +4,6 @@ package api
import (
"time"
"github.com/grafana/grafana/pkg/api/avatar"
"github.com/grafana/grafana/pkg/api/frontendlogging"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/log"
@@ -542,8 +541,7 @@ func (hs *HTTPServer) registerRoutes() {
r.Any("/api/gnet/*", reqSignedIn, hs.ProxyGnetRequest)
// Gravatar service.
avatarCacheServer := avatar.NewCacheServer(hs.Cfg)
r.Get("/avatar/:hash", avatarCacheServer.Handler)
r.Get("/avatar/:hash", hs.AvatarCacheServer.Handler)
// Snapshots
r.Post("/api/snapshots/", reqSnapshotPublicModeOrSignedIn, hs.CreateDashboardSnapshot)
+134 -59
View File
@@ -34,22 +34,31 @@ const (
// Avatar represents the avatar object.
type Avatar struct {
hash string
reqParams string
data *bytes.Buffer
notFound bool
isCustom bool
timestamp time.Time
}
var alog = log.New("avatar")
var (
alog = log.New("avatar")
// Represents a singleton AvatarCacheServer instance
csi *AvatarCacheServer
// Paremeters needed to fetch Gravatar with a retro fallback
gravatarReqParams = url.Values{
"d": {"retro"},
"size": {"200"},
"r": {"pg"},
}.Encode()
// Parameters needed to see if a Gravatar is custom
hasCustomReqParams = url.Values{
"d": {"404"},
}.Encode()
cacheInitOnce sync.Once
)
func New(hash string) *Avatar {
return &Avatar{
hash: hash,
reqParams: url.Values{
"d": {"retro"},
"size": {"200"},
"r": {"pg"}}.Encode(),
}
return &Avatar{hash: hash}
}
func (a *Avatar) Expired() bool {
@@ -61,16 +70,27 @@ func (a *Avatar) Encode(wr io.Writer) error {
return err
}
func (a *Avatar) Update() (err error) {
func (a *Avatar) update(baseUrl string) (err error) {
customUrl := baseUrl + a.hash + "?"
select {
case <-time.After(time.Second * 3):
err = fmt.Errorf("get gravatar image %s timeout", a.hash)
case err = <-thunder.GoFetch(gravatarSource+a.hash+"?"+a.reqParams, a):
case err = <-thunder.GoFetch(customUrl, a):
}
return err
}
type CacheServer struct {
func (a *Avatar) GetIsCustom() bool {
return a.isCustom
}
// Quick error handler to avoid multiple copy pastes
func (a *Avatar) setAvatarNotFound() {
a.notFound = true
a.isCustom = false
}
type AvatarCacheServer struct {
cfg *setting.Cfg
notFound *Avatar
cache *gocache.Cache
@@ -78,7 +98,7 @@ type CacheServer struct {
var validMD5 = regexp.MustCompile("^[a-fA-F0-9]{32}$")
func (a *CacheServer) Handler(ctx *models.ReqContext) {
func (a *AvatarCacheServer) Handler(ctx *models.ReqContext) {
hash := web.Params(ctx.Req)[":hash"]
if len(hash) != 32 || !validMD5.MatchString(hash) {
@@ -86,29 +106,7 @@ func (a *CacheServer) Handler(ctx *models.ReqContext) {
return
}
var avatar *Avatar
obj, exists := a.cache.Get(hash)
if exists {
avatar = obj.(*Avatar)
} else {
avatar = New(hash)
}
if avatar.Expired() {
// The cache item is either expired or newly created, update it from the server
if err := avatar.Update(); err != nil {
ctx.Logger.Debug("avatar update", "err", err)
avatar = a.notFound
}
}
if avatar.notFound {
avatar = a.notFound
} else if !exists {
if err := a.cache.Add(hash, avatar, gocache.DefaultExpiration); err != nil {
ctx.Logger.Debug("add avatar to cache", "err", err)
}
}
avatar := a.GetAvatarForHash(hash)
ctx.Resp.Header().Set("Content-Type", "image/jpeg")
@@ -120,12 +118,56 @@ func (a *CacheServer) Handler(ctx *models.ReqContext) {
if err := avatar.Encode(ctx.Resp); err != nil {
ctx.Logger.Warn("avatar encode error:", "err", err)
ctx.Resp.WriteHeader(500)
ctx.Resp.WriteHeader(http.StatusInternalServerError)
}
}
func NewCacheServer(cfg *setting.Cfg) *CacheServer {
return &CacheServer{
func (a *AvatarCacheServer) GetAvatarForHash(hash string) *Avatar {
if setting.DisableGravatar {
alog.Warn("'GetGravatarForHash' called despite gravatars being disabled; returning default profile image")
return a.notFound
}
return a.getAvatarForHash(hash, gravatarSource)
}
func (a *AvatarCacheServer) getAvatarForHash(hash string, baseUrl string) *Avatar {
var avatar *Avatar
obj, exists := a.cache.Get(hash)
if exists {
avatar = obj.(*Avatar)
} else {
avatar = New(hash)
}
if avatar.Expired() {
// The cache item is either expired or newly created, update it from the server
if err := avatar.update(baseUrl); err != nil {
alog.Debug("avatar update", "err", err)
avatar = a.notFound
}
}
if avatar.notFound {
avatar = a.notFound
} else if !exists {
if err := a.cache.Add(hash, avatar, gocache.DefaultExpiration); err != nil {
alog.Debug("add avatar to cache", "err", err)
}
}
return avatar
}
// Access cache server singleton instance
func ProvideAvatarCacheServer(cfg *setting.Cfg) *AvatarCacheServer {
cacheInitOnce.Do(func() {
csi = newCacheServer(cfg)
})
return csi
}
func newCacheServer(cfg *setting.Cfg) *AvatarCacheServer {
return &AvatarCacheServer{
cfg: cfg,
notFound: newNotFound(cfg),
cache: gocache.New(time.Hour, time.Hour*2),
@@ -133,7 +175,10 @@ func NewCacheServer(cfg *setting.Cfg) *CacheServer {
}
func newNotFound(cfg *setting.Cfg) *Avatar {
avatar := &Avatar{notFound: true}
avatar := &Avatar{
notFound: true,
isCustom: false,
}
// load user_profile png into buffer
// It's safe to ignore gosec warning G304 since the variable part of the file path comes from a configuration
@@ -176,11 +221,11 @@ func (t *Thunder) init() {
}
}
func (t *Thunder) Fetch(url string, avatar *Avatar) error {
func (t *Thunder) Fetch(baseUrl string, avatar *Avatar) error {
t.once.Do(t.init)
task := &thunderTask{
Url: url,
Avatar: avatar,
BaseUrl: baseUrl,
Avatar: avatar,
}
task.Add(1)
t.q <- task
@@ -188,18 +233,18 @@ func (t *Thunder) Fetch(url string, avatar *Avatar) error {
return task.err
}
func (t *Thunder) GoFetch(url string, avatar *Avatar) chan error {
func (t *Thunder) GoFetch(baseUrl string, avatar *Avatar) chan error {
c := make(chan error)
go func() {
c <- t.Fetch(url, avatar)
c <- t.Fetch(baseUrl, avatar)
}()
return c
}
// thunder download
type thunderTask struct {
Url string
Avatar *Avatar
BaseUrl string
Avatar *Avatar
sync.WaitGroup
err error
}
@@ -214,11 +259,48 @@ var client = &http.Client{
Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
}
// We fetch the same url with param tweaks twice in a row
// Break out the fetch function in a way that makes each
// Portion highly reusable
func (a *thunderTask) fetch() error {
a.Avatar.timestamp = time.Now()
alog.Debug("avatar.fetch(fetch new avatar)", "url", a.Url)
req, err := http.NewRequest("GET", a.Url, nil)
alog.Debug("avatar.fetch(fetch new avatar)", "url", a.BaseUrl)
// First do the fetch to get the Gravatar with a retro icon fallback
err := performGet(a.BaseUrl+gravatarReqParams, a.Avatar, getGravatarHandler)
if err == nil {
// Next do a fetch with a 404 fallback to see if it's a custom gravatar
return performGet(a.BaseUrl+hasCustomReqParams, a.Avatar, checkIsCustomHandler)
}
return err
}
type ResponseHandler func(av *Avatar, resp *http.Response) error
// Verifies the Gravatar response code was 200, then stores the image byte slice
func getGravatarHandler(av *Avatar, resp *http.Response) error {
if resp.StatusCode != http.StatusOK {
av.setAvatarNotFound()
return fmt.Errorf("status code: %d", resp.StatusCode)
}
av.data = &bytes.Buffer{}
writer := bufio.NewWriter(av.data)
_, err := io.Copy(writer, resp.Body)
return err
}
// Uses the d=404 fallback to see if the gravatar we got back is custom
func checkIsCustomHandler(av *Avatar, resp *http.Response) error {
av.isCustom = resp.StatusCode != http.StatusNotFound
return nil
}
// Reusable Get helper that allows us to pass in custom handling depending on the endpoint
func performGet(url string, av *Avatar, handler ResponseHandler) error {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
}
@@ -227,9 +309,10 @@ func (a *thunderTask) fetch() error {
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.8")
req.Header.Set("Cache-Control", "no-cache")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.154 Safari/537.36")
alog.Debug("Fetching avatar url with parameters", "url", url)
resp, err := client.Do(req)
if err != nil {
a.Avatar.notFound = true
av.setAvatarNotFound()
return fmt.Errorf("gravatar unreachable: %w", err)
}
defer func() {
@@ -238,14 +321,6 @@ func (a *thunderTask) fetch() error {
}
}()
if resp.StatusCode != 200 {
a.Avatar.notFound = true
return fmt.Errorf("status code: %d", resp.StatusCode)
}
a.Avatar.data = &bytes.Buffer{}
writer := bufio.NewWriter(a.Avatar.data)
_, err = io.Copy(writer, resp.Body)
err = handler(av, resp)
return err
}
+120
View File
@@ -0,0 +1,120 @@
package avatar
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require"
)
const DEFAULT_NONSENSE_HASH string = "9e107d9d372bb6826bd81d3542a419d6"
const CUSTOM_NONSENSE_HASH string = "d2a9116d4a63304733ca0f3471e57d16"
var NONSENSE_BODY []byte = []byte("Bogus API response")
func TestAvatar_AvatarRetrieval(t *testing.T) {
avc := ProvideAvatarCacheServer(setting.NewCfg())
callCounter := 0
mockServer := setupMockGravatarServer(&callCounter, false)
t.Cleanup(func() {
avc.cache.Flush()
mockServer.Close()
})
av := avc.getAvatarForHash(DEFAULT_NONSENSE_HASH, mockServer.URL+"/avatar/")
// verify there was a call to get the image and a call to the 404 fallback
require.Equal(t, callCounter, 2)
require.Equal(t, av.data.Bytes(), NONSENSE_BODY)
avc.getAvatarForHash(DEFAULT_NONSENSE_HASH, mockServer.URL+"/avatar/")
//since the avatar is cached, there should not have been anymore REST calls
require.Equal(t, callCounter, 2)
}
func TestAvatar_CheckCustom(t *testing.T) {
avc := ProvideAvatarCacheServer(setting.NewCfg())
callCounter := 0
mockServer := setupMockGravatarServer(&callCounter, false)
t.Cleanup(func() {
avc.cache.Flush()
mockServer.Close()
})
av := avc.getAvatarForHash(DEFAULT_NONSENSE_HASH, mockServer.URL+"/avatar/")
// verify this avatar is not marked custom
require.False(t, av.isCustom)
av2 := avc.getAvatarForHash(CUSTOM_NONSENSE_HASH, mockServer.URL+"/avatar/")
// verify this avatar is marked custom
require.True(t, av2.isCustom)
}
func TestAvatar_FallbackCase(t *testing.T) {
avc := ProvideAvatarCacheServer(setting.NewCfg())
callCounter := 0
mockServer := setupMockGravatarServer(&callCounter, true)
t.Cleanup(func() {
avc.cache.Flush()
mockServer.Close()
})
av := avc.getAvatarForHash(DEFAULT_NONSENSE_HASH, mockServer.URL+"/avatar/")
// the client should not have gotten a valid response back from the first call
// there should only be one REST call, and the avatar url should be the default
require.Equal(t, callCounter, 1)
require.False(t, av.isCustom)
require.True(t, av.notFound)
require.Equal(t, av, avc.notFound)
}
func TestAvatar_ExpirationHandler(t *testing.T) {
avc := ProvideAvatarCacheServer(setting.NewCfg())
callCounter := 0
mockServer := setupMockGravatarServer(&callCounter, false)
t.Cleanup(func() {
avc.cache.Flush()
mockServer.Close()
})
av := avc.getAvatarForHash(DEFAULT_NONSENSE_HASH, mockServer.URL+"/avatar/")
// verify there was a call to get the image and a call to the 404 fallback
require.Equal(t, callCounter, 2)
require.Equal(t, av.data.Bytes(), NONSENSE_BODY)
// manually expire the avatar in the cache
av.timestamp = av.timestamp.Add(-time.Minute * 15)
avc.getAvatarForHash(DEFAULT_NONSENSE_HASH, mockServer.URL+"/avatar/")
//since the avatar is expired, there should be two more REST calls
require.Equal(t, callCounter, 4)
}
func setupMockGravatarServer(counter *int, simulateError bool) *httptest.Server {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
(*counter)++
splitUri := strings.Split(r.RequestURI, "?")
urlHash := splitUri[0][len("/avatar/"):]
params := splitUri[1]
if params == "d=404" {
if urlHash == DEFAULT_NONSENSE_HASH {
w.WriteHeader(404)
} else {
_, _ = w.Write(NONSENSE_BODY)
}
} else {
if simulateError {
w.WriteHeader(500)
} else {
_, _ = w.Write(NONSENSE_BODY)
}
}
}))
return server
}
+4 -2
View File
@@ -34,6 +34,7 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ldap"
"github.com/grafana/grafana/pkg/services/login/loginservice"
"github.com/grafana/grafana/pkg/services/login/logintest"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/searchusers"
@@ -168,7 +169,7 @@ type scenarioContext struct {
url string
userAuthTokenService *auth.FakeUserAuthTokenService
sqlStore sqlstore.Store
authInfoService *mockAuthInfoService
authInfoService *logintest.AuthInfoServiceFake
}
func (sc *scenarioContext) exec() {
@@ -399,8 +400,9 @@ func setupHTTPServerWithCfgDb(t *testing.T, useFakeAccessControl, enableAccessCo
require.NoError(t, err)
hs.teamPermissionsService = teamPermissionService
} else {
ac := ossaccesscontrol.ProvideService(hs.Features, &usagestats.UsageStatsMock{T: t},
ac, errInitAc := ossaccesscontrol.ProvideService(hs.Features, &usagestats.UsageStatsMock{T: t},
database.ProvideService(db), routing.NewRouteRegister())
require.NoError(t, errInitAc)
hs.AccessControl = ac
// Perform role registration
err := hs.declareFixedRoles()
+7 -1
View File
@@ -202,7 +202,13 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response {
func (hs *HTTPServer) getAnnotationPermissionsByScope(c *models.ReqContext, actions *dtos.AnnotationActions, scope string) {
var err error
evaluate := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsDelete, scope)
evaluate := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, scope)
actions.CanAdd, err = hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluate)
if err != nil {
hs.log.Warn("Failed to evaluate permission", "err", err, "action", accesscontrol.ActionAnnotationsCreate, "scope", scope)
}
evaluate = accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsDelete, scope)
actions.CanDelete, err = hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluate)
if err != nil {
hs.log.Warn("Failed to evaluate permission", "err", err, "action", accesscontrol.ActionAnnotationsDelete, "scope", scope)
+1
View File
@@ -40,6 +40,7 @@ type AnnotationPermission struct {
}
type AnnotationActions struct {
CanAdd bool `json:"canAdd"`
CanEdit bool `json:"canEdit"`
CanDelete bool `json:"canDelete"`
}
+11 -1
View File
@@ -28,6 +28,7 @@ type LoginCommand struct {
type CurrentUser struct {
IsSignedIn bool `json:"isSignedIn"`
Id int64 `json:"id"`
ExternalUserId string `json:"externalUserId"`
Login string `json:"login"`
Email string `json:"email"`
Name string `json:"name"`
@@ -78,11 +79,20 @@ func GetGravatarUrl(text string) string {
return ""
}
hash, _ := GetGravatarHash(text)
return fmt.Sprintf(setting.AppSubUrl+"/avatar/%x", hash)
}
func GetGravatarHash(text string) ([]byte, bool) {
if text == "" {
return make([]byte, 0), false
}
hasher := md5.New()
if _, err := hasher.Write([]byte(strings.ToLower(text))); err != nil {
mlog.Warn("Failed to hash text", "err", err)
}
return fmt.Sprintf(setting.AppSubUrl+"/avatar/%x", hasher.Sum(nil))
return hasher.Sum(nil), true
}
func GetGravatarUrlWithDefault(text string, defaultText string) string {
+8 -1
View File
@@ -13,6 +13,7 @@ import (
"strings"
"sync"
"github.com/grafana/grafana/pkg/api/avatar"
"github.com/grafana/grafana/pkg/api/routing"
httpstatic "github.com/grafana/grafana/pkg/api/static"
"github.com/grafana/grafana/pkg/bus"
@@ -21,6 +22,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/infra/tracing"
loginpkg "github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
@@ -134,6 +136,7 @@ type HTTPServer struct {
queryDataService *query.Service
serviceAccountsService serviceaccounts.Service
authInfoService login.AuthInfoService
authenticator loginpkg.Authenticator
teamPermissionsService accesscontrol.PermissionsService
permissionServices accesscontrol.PermissionsServices
NotificationService *notifications.NotificationService
@@ -145,6 +148,7 @@ type HTTPServer struct {
AlertNotificationService *alerting.AlertNotificationService
DashboardsnapshotsService *dashboardsnapshots.Service
PluginSettings *pluginSettings.Service
AvatarCacheServer *avatar.AvatarCacheServer
}
type ServerOptions struct {
@@ -160,7 +164,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
dataSourceCache datasources.CacheService, userTokenService models.UserTokenService,
cleanUpService *cleanup.CleanUpService, shortURLService shorturls.Service, queryHistoryService queryhistory.Service,
thumbService thumbs.Service, remoteCache *remotecache.RemoteCache, provisioningService provisioning.ProvisioningService,
loginService login.Service, accessControl accesscontrol.AccessControl,
loginService login.Service, authenticator loginpkg.Authenticator, accessControl accesscontrol.AccessControl,
dataSourceProxy *datasourceproxy.DataSourceProxyService, searchService *search.SearchService,
live *live.GrafanaLive, livePushGateway *pushhttp.Gateway, plugCtxProvider *plugincontext.Provider,
contextHandler *contexthandler.ContextHandler, features *featuremgmt.FeatureManager,
@@ -176,6 +180,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
dashboardProvisioningService dashboards.DashboardProvisioningService, folderService dashboards.FolderService,
datasourcePermissionsService permissions.DatasourcePermissionsService, alertNotificationService *alerting.AlertNotificationService,
dashboardsnapshotsService *dashboardsnapshots.Service, commentsService *comments.Service, pluginSettings *pluginSettings.Service,
avatarCacheServer *avatar.AvatarCacheServer,
) (*HTTPServer, error) {
web.Env = cfg.Env
m := web.New()
@@ -236,6 +241,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
queryDataService: queryDataService,
serviceAccountsService: serviceaccountsService,
authInfoService: authInfoService,
authenticator: authenticator,
NotificationService: notificationService,
dashboardService: dashboardService,
dashboardProvisioningService: dashboardProvisioningService,
@@ -247,6 +253,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
DashboardsnapshotsService: dashboardsnapshotsService,
PluginSettings: pluginSettings,
permissionServices: permissionsServices,
AvatarCacheServer: avatarCacheServer,
}
if hs.Listener != nil {
hs.log.Debug("Using provided listener")
+8 -4
View File
@@ -120,11 +120,14 @@ func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error)
}
if include.Type == "dashboard" && include.AddToNav {
link := &dtos.NavLink{
Url: hs.Cfg.AppSubURL + include.GetSlugOrUIDLink(),
Text: include.Name,
dboardURL := include.DashboardURLPath()
if dboardURL != "" {
link := &dtos.NavLink{
Url: path.Join(hs.Cfg.AppSubURL, dboardURL),
Text: include.Name,
}
appLink.Children = append(appLink.Children, link)
}
appLink.Children = append(appLink.Children, link)
}
}
@@ -653,6 +656,7 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
IsSignedIn: c.IsSignedIn,
Login: c.Login,
Email: c.Email,
ExternalUserId: c.SignedInUser.ExternalAuthId,
Name: c.Name,
OrgCount: c.OrgCount,
OrgId: c.OrgId,
+1 -2
View File
@@ -9,7 +9,6 @@ import (
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/ldap"
"github.com/grafana/grafana/pkg/services/multildap"
@@ -199,7 +198,7 @@ func (hs *HTTPServer) PostSyncUserWithLDAP(c *models.ReqContext) response.Respon
}
// Since the user was not in the LDAP server. Let's disable it.
err := login.DisableExternalUser(c.Req.Context(), query.Result.Login)
err := hs.Login.DisableExternalUser(c.Req.Context(), query.Result.Login)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to disable the user", err)
}
+6 -16
View File
@@ -1,7 +1,6 @@
package api
import (
"context"
"errors"
"net/http"
"net/http/httptest"
@@ -10,12 +9,12 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/login/loginservice"
"github.com/grafana/grafana/pkg/services/login/logintest"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/ldap"
@@ -368,6 +367,7 @@ func postSyncUserWithLDAPContext(t *testing.T, requestURL string, preHook func(*
t.Helper()
sc := setupScenarioContext(t, requestURL)
sc.authInfoService = &logintest.AuthInfoServiceFake{}
ldap := setting.LDAPEnabled
t.Cleanup(func() {
@@ -380,7 +380,7 @@ func postSyncUserWithLDAPContext(t *testing.T, requestURL string, preHook func(*
AuthTokenService: auth.NewFakeUserAuthTokenService(),
SQLStore: sqlstoremock,
Login: loginservice.LoginServiceMock{},
authInfoService: &mockAuthInfoService{},
authInfoService: sc.authInfoService,
}
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
@@ -483,6 +483,7 @@ func TestPostSyncUserWithLDAPAPIEndpoint_WhenGrafanaAdmin(t *testing.T) {
func TestPostSyncUserWithLDAPAPIEndpoint_WhenUserNotInLDAP(t *testing.T) {
sqlstoremock := mockstore.SQLStoreMock{ExpectedUser: &models.User{Login: "ldap-daniel", Id: 34}}
sc := postSyncUserWithLDAPContext(t, "/api/admin/ldap/sync/34", func(t *testing.T, sc *scenarioContext) {
sc.authInfoService.ExpectedExternalUser = &models.ExternalUserInfo{IsDisabled: true, UserId: 34}
getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) {
return &ldap.Config{}, nil
}
@@ -492,18 +493,7 @@ func TestPostSyncUserWithLDAPAPIEndpoint_WhenUserNotInLDAP(t *testing.T) {
}
userSearchResult = nil
bus.AddHandler("test", func(ctx context.Context, q *models.GetExternalUserInfoByLoginQuery) error {
assert.Equal(t, "ldap-daniel", q.LoginOrEmail)
q.Result = &models.ExternalUserInfo{IsDisabled: true, UserId: 34}
return nil
})
bus.AddHandler("test", func(ctx context.Context, cmd *models.DisableUserCommand) error {
assert.Equal(t, 34, cmd.UserId)
return nil
})
userSearchError = multildap.ErrDidNotFindUser
}, &sqlstoremock)
assert.Equal(t, http.StatusBadRequest, sc.resp.Code)
@@ -616,7 +606,7 @@ func TestLDAP_AccessControl(t *testing.T) {
cfg.LDAPEnabled = true
sc, hs := setupAccessControlScenarioContext(t, cfg, test.url, test.permissions)
hs.SQLStore = &mockstore.SQLStoreMock{ExpectedUser: &models.User{}}
hs.authInfoService = &mockAuthInfoService{}
hs.authInfoService = &logintest.AuthInfoServiceFake{}
hs.Login = &loginservice.LoginServiceMock{}
sc.resp = httptest.NewRecorder()
sc.req, err = http.NewRequest(test.method, test.url, nil)
+1 -1
View File
@@ -209,7 +209,7 @@ func (hs *HTTPServer) LoginPost(c *models.ReqContext) response.Response {
Cfg: hs.Cfg,
}
err := login.AuthenticateUserFunc(c.Req.Context(), authQuery)
err := hs.authenticator.AuthenticateUser(c.Req.Context(), authQuery)
authModule = authQuery.AuthModule
if err != nil {
resp = response.Error(401, "Invalid username or password", err)
+11 -12
View File
@@ -351,8 +351,7 @@ func TestLoginPostRedirect(t *testing.T) {
Email: "",
}
mockAuthenticateUserFunc(user, "", nil)
t.Cleanup(resetAuthenticateUserFunc)
hs.authenticator = &fakeAuthenticator{user, "", nil}
redirectCases := []redirectCase{
{
@@ -684,8 +683,7 @@ func TestLoginPostRunLokingHook(t *testing.T) {
for _, c := range testCases {
t.Run(c.desc, func(t *testing.T) {
mockAuthenticateUserFunc(c.authUser, c.authModule, c.authErr)
t.Cleanup(resetAuthenticateUserFunc)
hs.authenticator = &fakeAuthenticator{c.authUser, c.authModule, c.authErr}
sc.m.Post(sc.url, sc.defaultHandler)
sc.fakeReqNoAssertions("POST", sc.url).exec()
@@ -732,13 +730,14 @@ func (m *mockSocialService) GetConnector(string) (social.SocialConnector, error)
return m.socialConnector, m.err
}
func mockAuthenticateUserFunc(user *models.User, authmodule string, err error) {
login.AuthenticateUserFunc = func(ctx context.Context, query *models.LoginUserQuery) error {
query.User = user
query.AuthModule = authmodule
return err
}
type fakeAuthenticator struct {
ExpectedUser *models.User
ExpectedAuthModule string
ExpectedError error
}
func resetAuthenticateUserFunc() {
login.AuthenticateUserFunc = login.AuthenticateUser
func (fa *fakeAuthenticator) AuthenticateUser(c context.Context, query *models.LoginUserQuery) error {
query.User = fa.ExpectedUser
query.AuthModule = fa.ExpectedAuthModule
return fa.ExpectedError
}
+25
View File
@@ -506,6 +506,31 @@ func (hs *HTTPServer) callPluginResource(c *models.ReqContext, pluginID, dsUID s
}
clonedReq.URL = urlPath
if dsUID != "" {
ds, err := hs.DataSourceCache.GetDatasourceByUID(c.Req.Context(), dsUID, c.SignedInUser, c.SkipCache)
if err != nil {
if errors.Is(err, models.ErrDataSourceNotFound) {
c.JsonApiErr(404, "Datasource not found", err)
return
}
c.JsonApiErr(500, "Failed to get datasource", err)
return
}
if hs.DataProxy.OAuthTokenService.IsOAuthPassThruEnabled(ds) {
if token := hs.DataProxy.OAuthTokenService.GetCurrentOAuthToken(c.Req.Context(), c.SignedInUser); token != nil {
clonedReq.Header.Add("Authorization", fmt.Sprintf("%s %s", token.Type(), token.AccessToken))
idToken, ok := token.Extra("id_token").(string)
if ok && idToken != "" {
clonedReq.Header.Add("X-ID-Token", idToken)
}
}
}
}
if err = hs.makePluginResourceRequest(c.Resp, clonedReq, pCtx); err != nil {
handleCallResourceError(err, c)
}
+7
View File
@@ -5,12 +5,18 @@ import (
// remove the cron (v1) dependency
_ "cloud.google.com/go/kms/apiv1"
_ "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
_ "github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys"
_ "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault"
_ "github.com/Azure/go-autorest/autorest"
_ "github.com/Azure/go-autorest/autorest/adal"
_ "github.com/beevik/etree"
_ "github.com/cortexproject/cortex/pkg/util"
_ "github.com/crewjam/saml"
_ "github.com/gobwas/glob"
_ "github.com/googleapis/gax-go/v2"
_ "github.com/grafana/dskit/backoff"
_ "github.com/grafana/dskit/flagext"
_ "github.com/grafana/loki/clients/pkg/promtail/client"
_ "github.com/grafana/loki/pkg/logproto"
_ "github.com/grpc-ecosystem/go-grpc-middleware"
@@ -24,6 +30,7 @@ import (
_ "github.com/stretchr/testify/require"
_ "github.com/vectordotdev/go-datemath"
_ "golang.org/x/time/rate"
_ "google.golang.org/genproto/googleapis/cloud/kms/v1"
_ "gopkg.in/square/go-jose.v2"
)
+5 -2
View File
@@ -7,6 +7,7 @@ package log
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"sort"
@@ -41,8 +42,10 @@ func init() {
loggersToClose = make([]DisposableHandler, 0)
loggersToReload = make([]ReloadableHandler, 0)
// Use console by default
format := getLogFormat("console")
// Use discard by default
format := func(w io.Writer) gokitlog.Logger {
return gokitlog.NewLogfmtLogger(gokitlog.NewSyncWriter(ioutil.Discard))
}
logger := level.NewFilter(format(os.Stderr), level.AllowInfo())
root = newManager(logger)
}
+22 -8
View File
@@ -8,6 +8,8 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/ldap"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/sqlstore"
)
var (
@@ -25,15 +27,27 @@ var (
var loginLogger = log.New("login")
var AuthenticateUserFunc = AuthenticateUser
type Authenticator interface {
AuthenticateUser(context.Context, *models.LoginUserQuery) error
}
func Init() {
bus.AddHandler("auth", AuthenticateUser)
type AuthenticatorService struct {
store sqlstore.Store
loginService login.Service
}
func ProvideService(store sqlstore.Store, loginService login.Service) *AuthenticatorService {
a := &AuthenticatorService{
store: store,
loginService: loginService,
}
bus.AddHandler("auth", a.AuthenticateUser)
return a
}
// AuthenticateUser authenticates the user via username & password
func AuthenticateUser(ctx context.Context, query *models.LoginUserQuery) error {
if err := validateLoginAttempts(ctx, query); err != nil {
func (a *AuthenticatorService) AuthenticateUser(ctx context.Context, query *models.LoginUserQuery) error {
if err := validateLoginAttempts(ctx, query, a.store); err != nil {
return err
}
@@ -41,14 +55,14 @@ func AuthenticateUser(ctx context.Context, query *models.LoginUserQuery) error {
return err
}
err := loginUsingGrafanaDB(ctx, query)
err := loginUsingGrafanaDB(ctx, query, a.store)
if err == nil || (!errors.Is(err, models.ErrUserNotFound) && !errors.Is(err, ErrInvalidCredentials) &&
!errors.Is(err, ErrUserDisabled)) {
query.AuthModule = "grafana"
return err
}
ldapEnabled, ldapErr := loginUsingLDAP(ctx, query)
ldapEnabled, ldapErr := loginUsingLDAP(ctx, query, a.loginService)
if ldapEnabled {
query.AuthModule = models.AuthModuleLDAP
if ldapErr == nil || !errors.Is(ldapErr, ldap.ErrInvalidCredentials) {
@@ -61,7 +75,7 @@ func AuthenticateUser(ctx context.Context, query *models.LoginUserQuery) error {
}
if errors.Is(err, ErrInvalidCredentials) || errors.Is(err, ldap.ErrInvalidCredentials) {
if err := saveInvalidLoginAttempt(ctx, query); err != nil {
if err := saveInvalidLoginAttempt(ctx, query, a.store); err != nil {
loginLogger.Error("Failed to save invalid login attempt", "err", err)
}
+26 -13
View File
@@ -7,6 +7,10 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/ldap"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/login/logintest"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -21,7 +25,8 @@ func TestAuthenticateUser(t *testing.T) {
Username: "user",
Password: "",
}
err := AuthenticateUserFunc(context.Background(), &loginQuery)
a := AuthenticatorService{store: mockstore.NewSQLStoreMock(), loginService: &logintest.LoginServiceFake{}}
err := a.AuthenticateUser(context.Background(), &loginQuery)
require.EqualError(t, err, ErrPasswordEmpty.Error())
assert.False(t, sc.grafanaLoginWasCalled)
@@ -35,7 +40,8 @@ func TestAuthenticateUser(t *testing.T) {
mockLoginUsingLDAP(true, nil, sc)
mockSaveInvalidLoginAttempt(sc)
err := AuthenticateUserFunc(context.Background(), sc.loginUserQuery)
a := AuthenticatorService{store: mockstore.NewSQLStoreMock(), loginService: &logintest.LoginServiceFake{}}
err := a.AuthenticateUser(context.Background(), sc.loginUserQuery)
require.EqualError(t, err, ErrTooManyLoginAttempts.Error())
assert.True(t, sc.loginAttemptValidationWasCalled)
@@ -51,7 +57,8 @@ func TestAuthenticateUser(t *testing.T) {
mockLoginUsingLDAP(true, ErrInvalidCredentials, sc)
mockSaveInvalidLoginAttempt(sc)
err := AuthenticateUserFunc(context.Background(), sc.loginUserQuery)
a := AuthenticatorService{store: mockstore.NewSQLStoreMock(), loginService: &logintest.LoginServiceFake{}}
err := a.AuthenticateUser(context.Background(), sc.loginUserQuery)
require.NoError(t, err)
assert.True(t, sc.loginAttemptValidationWasCalled)
@@ -68,7 +75,8 @@ func TestAuthenticateUser(t *testing.T) {
mockLoginUsingLDAP(true, ErrInvalidCredentials, sc)
mockSaveInvalidLoginAttempt(sc)
err := AuthenticateUserFunc(context.Background(), sc.loginUserQuery)
a := AuthenticatorService{store: mockstore.NewSQLStoreMock(), loginService: &logintest.LoginServiceFake{}}
err := a.AuthenticateUser(context.Background(), sc.loginUserQuery)
require.EqualError(t, err, customErr.Error())
assert.True(t, sc.loginAttemptValidationWasCalled)
@@ -84,7 +92,8 @@ func TestAuthenticateUser(t *testing.T) {
mockLoginUsingLDAP(false, nil, sc)
mockSaveInvalidLoginAttempt(sc)
err := AuthenticateUserFunc(context.Background(), sc.loginUserQuery)
a := AuthenticatorService{store: mockstore.NewSQLStoreMock(), loginService: &logintest.LoginServiceFake{}}
err := a.AuthenticateUser(context.Background(), sc.loginUserQuery)
require.EqualError(t, err, models.ErrUserNotFound.Error())
assert.True(t, sc.loginAttemptValidationWasCalled)
@@ -100,7 +109,8 @@ func TestAuthenticateUser(t *testing.T) {
mockLoginUsingLDAP(true, ldap.ErrInvalidCredentials, sc)
mockSaveInvalidLoginAttempt(sc)
err := AuthenticateUserFunc(context.Background(), sc.loginUserQuery)
a := AuthenticatorService{store: mockstore.NewSQLStoreMock(), loginService: &logintest.LoginServiceFake{}}
err := a.AuthenticateUser(context.Background(), sc.loginUserQuery)
require.EqualError(t, err, ErrInvalidCredentials.Error())
assert.True(t, sc.loginAttemptValidationWasCalled)
@@ -116,7 +126,8 @@ func TestAuthenticateUser(t *testing.T) {
mockLoginUsingLDAP(true, nil, sc)
mockSaveInvalidLoginAttempt(sc)
err := AuthenticateUserFunc(context.Background(), sc.loginUserQuery)
a := AuthenticatorService{store: mockstore.NewSQLStoreMock(), loginService: &logintest.LoginServiceFake{}}
err := a.AuthenticateUser(context.Background(), sc.loginUserQuery)
require.NoError(t, err)
assert.True(t, sc.loginAttemptValidationWasCalled)
@@ -133,7 +144,8 @@ func TestAuthenticateUser(t *testing.T) {
mockLoginUsingLDAP(true, customErr, sc)
mockSaveInvalidLoginAttempt(sc)
err := AuthenticateUserFunc(context.Background(), sc.loginUserQuery)
a := AuthenticatorService{store: mockstore.NewSQLStoreMock(), loginService: &logintest.LoginServiceFake{}}
err := a.AuthenticateUser(context.Background(), sc.loginUserQuery)
require.EqualError(t, err, customErr.Error())
assert.True(t, sc.loginAttemptValidationWasCalled)
@@ -149,7 +161,8 @@ func TestAuthenticateUser(t *testing.T) {
mockLoginUsingLDAP(true, ldap.ErrInvalidCredentials, sc)
mockSaveInvalidLoginAttempt(sc)
err := AuthenticateUserFunc(context.Background(), sc.loginUserQuery)
a := AuthenticatorService{store: mockstore.NewSQLStoreMock(), loginService: &logintest.LoginServiceFake{}}
err := a.AuthenticateUser(context.Background(), sc.loginUserQuery)
require.EqualError(t, err, ErrInvalidCredentials.Error())
assert.True(t, sc.loginAttemptValidationWasCalled)
@@ -170,28 +183,28 @@ type authScenarioContext struct {
type authScenarioFunc func(sc *authScenarioContext)
func mockLoginUsingGrafanaDB(err error, sc *authScenarioContext) {
loginUsingGrafanaDB = func(ctx context.Context, query *models.LoginUserQuery) error {
loginUsingGrafanaDB = func(ctx context.Context, query *models.LoginUserQuery, _ sqlstore.Store) error {
sc.grafanaLoginWasCalled = true
return err
}
}
func mockLoginUsingLDAP(enabled bool, err error, sc *authScenarioContext) {
loginUsingLDAP = func(ctx context.Context, query *models.LoginUserQuery) (bool, error) {
loginUsingLDAP = func(ctx context.Context, query *models.LoginUserQuery, _ login.Service) (bool, error) {
sc.ldapLoginWasCalled = true
return enabled, err
}
}
func mockLoginAttemptValidation(err error, sc *authScenarioContext) {
validateLoginAttempts = func(context.Context, *models.LoginUserQuery) error {
validateLoginAttempts = func(context.Context, *models.LoginUserQuery, sqlstore.Store) error {
sc.loginAttemptValidationWasCalled = true
return err
}
}
func mockSaveInvalidLoginAttempt(sc *authScenarioContext) {
saveInvalidLoginAttempt = func(ctx context.Context, query *models.LoginUserQuery) error {
saveInvalidLoginAttempt = func(ctx context.Context, query *models.LoginUserQuery, _ sqlstore.Store) error {
sc.saveInvalidLoginAttemptWasCalled = true
return nil
}
+5 -5
View File
@@ -4,8 +4,8 @@ import (
"context"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
)
var (
@@ -13,7 +13,7 @@ var (
loginAttemptsWindow = time.Minute * 5
)
var validateLoginAttempts = func(ctx context.Context, query *models.LoginUserQuery) error {
var validateLoginAttempts = func(ctx context.Context, query *models.LoginUserQuery, store sqlstore.Store) error {
if query.Cfg.DisableBruteForceLoginProtection {
return nil
}
@@ -23,7 +23,7 @@ var validateLoginAttempts = func(ctx context.Context, query *models.LoginUserQue
Since: time.Now().Add(-loginAttemptsWindow),
}
if err := bus.Dispatch(ctx, &loginAttemptCountQuery); err != nil {
if err := store.GetUserLoginAttemptCount(ctx, &loginAttemptCountQuery); err != nil {
return err
}
@@ -34,7 +34,7 @@ var validateLoginAttempts = func(ctx context.Context, query *models.LoginUserQue
return nil
}
var saveInvalidLoginAttempt = func(ctx context.Context, query *models.LoginUserQuery) error {
var saveInvalidLoginAttempt = func(ctx context.Context, query *models.LoginUserQuery, store sqlstore.Store) error {
if query.Cfg.DisableBruteForceLoginProtection {
return nil
}
@@ -44,5 +44,5 @@ var saveInvalidLoginAttempt = func(ctx context.Context, query *models.LoginUserQ
IpAddress: query.IpAddress,
}
return bus.Dispatch(ctx, &loginAttemptCommand)
return store.CreateLoginAttempt(ctx, &loginAttemptCommand)
}
+12 -33
View File
@@ -4,8 +4,8 @@ import (
"context"
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -59,11 +59,12 @@ func TestValidateLoginAttempts(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
withLoginAttempts(t, tc.loginAttempts)
store := mockstore.NewSQLStoreMock()
store.ExpectedLoginAttempts = tc.loginAttempts
query := &models.LoginUserQuery{Username: "user", Cfg: tc.cfg}
err := validateLoginAttempts(context.Background(), query)
err := validateLoginAttempts(context.Background(), query, store)
require.Equal(t, tc.expected, err)
})
}
@@ -71,45 +72,31 @@ func TestValidateLoginAttempts(t *testing.T) {
func TestSaveInvalidLoginAttempt(t *testing.T) {
t.Run("When brute force protection enabled", func(t *testing.T) {
t.Cleanup(func() { bus.ClearBusHandlers() })
createLoginAttemptCmd := &models.CreateLoginAttemptCommand{}
bus.AddHandler("test", func(ctx context.Context, cmd *models.CreateLoginAttemptCommand) error {
createLoginAttemptCmd = cmd
return nil
})
store := mockstore.NewSQLStoreMock()
err := saveInvalidLoginAttempt(context.Background(), &models.LoginUserQuery{
Username: "user",
Password: "pwd",
IpAddress: "192.168.1.1:56433",
Cfg: cfgWithBruteForceLoginProtectionEnabled(t),
})
}, store)
require.NoError(t, err)
require.NotNil(t, createLoginAttemptCmd)
assert.Equal(t, "user", createLoginAttemptCmd.Username)
assert.Equal(t, "192.168.1.1:56433", createLoginAttemptCmd.IpAddress)
require.NotNil(t, store.LastLoginAttemptCommand)
assert.Equal(t, "user", store.LastLoginAttemptCommand.Username)
assert.Equal(t, "192.168.1.1:56433", store.LastLoginAttemptCommand.IpAddress)
})
t.Run("When brute force protection disabled", func(t *testing.T) {
t.Cleanup(func() { bus.ClearBusHandlers() })
var createLoginAttemptCmd *models.CreateLoginAttemptCommand
bus.AddHandler("test", func(ctx context.Context, cmd *models.CreateLoginAttemptCommand) error {
createLoginAttemptCmd = cmd
return nil
})
store := mockstore.NewSQLStoreMock()
err := saveInvalidLoginAttempt(context.Background(), &models.LoginUserQuery{
Username: "user",
Password: "pwd",
IpAddress: "192.168.1.1:56433",
Cfg: cfgWithBruteForceLoginProtectionDisabled(t),
})
}, store)
require.NoError(t, err)
require.Nil(t, createLoginAttemptCmd)
require.Nil(t, store.LastLoginAttemptCommand)
})
}
@@ -126,11 +113,3 @@ func cfgWithBruteForceLoginProtectionEnabled(t *testing.T) *setting.Cfg {
require.False(t, cfg.DisableBruteForceLoginProtection)
return cfg
}
func withLoginAttempts(t *testing.T, loginAttempts int64) {
t.Helper()
bus.AddHandler("test", func(ctx context.Context, query *models.GetUserLoginAttemptCountQuery) error {
query.Result = loginAttempts
return nil
})
}
+3 -3
View File
@@ -4,8 +4,8 @@ import (
"context"
"crypto/subtle"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/util"
)
@@ -21,10 +21,10 @@ var validatePassword = func(providedPassword string, userPassword string, userSa
return nil
}
var loginUsingGrafanaDB = func(ctx context.Context, query *models.LoginUserQuery) error {
var loginUsingGrafanaDB = func(ctx context.Context, query *models.LoginUserQuery, store sqlstore.Store) error {
userQuery := models.GetUserByLoginQuery{LoginOrEmail: query.Username}
if err := bus.Dispatch(ctx, &userQuery); err != nil {
if err := store.GetUserByLogin(ctx, &userQuery); err != nil {
return err
}
+11 -13
View File
@@ -4,8 +4,8 @@ import (
"context"
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -13,7 +13,7 @@ import (
func TestLoginUsingGrafanaDB(t *testing.T) {
grafanaLoginScenario(t, "When login with non-existing user", func(sc *grafanaLoginScenarioContext) {
sc.withNonExistingUser()
err := loginUsingGrafanaDB(context.Background(), sc.loginUserQuery)
err := loginUsingGrafanaDB(context.Background(), sc.loginUserQuery, sc.store)
require.EqualError(t, err, models.ErrUserNotFound.Error())
assert.False(t, sc.validatePasswordCalled)
@@ -22,7 +22,7 @@ func TestLoginUsingGrafanaDB(t *testing.T) {
grafanaLoginScenario(t, "When login with invalid credentials", func(sc *grafanaLoginScenarioContext) {
sc.withInvalidPassword()
err := loginUsingGrafanaDB(context.Background(), sc.loginUserQuery)
err := loginUsingGrafanaDB(context.Background(), sc.loginUserQuery, sc.store)
require.EqualError(t, err, ErrInvalidCredentials.Error())
@@ -32,7 +32,7 @@ func TestLoginUsingGrafanaDB(t *testing.T) {
grafanaLoginScenario(t, "When login with valid credentials", func(sc *grafanaLoginScenarioContext) {
sc.withValidCredentials()
err := loginUsingGrafanaDB(context.Background(), sc.loginUserQuery)
err := loginUsingGrafanaDB(context.Background(), sc.loginUserQuery, sc.store)
require.NoError(t, err)
assert.True(t, sc.validatePasswordCalled)
@@ -44,7 +44,7 @@ func TestLoginUsingGrafanaDB(t *testing.T) {
grafanaLoginScenario(t, "When login with disabled user", func(sc *grafanaLoginScenarioContext) {
sc.withDisabledUser()
err := loginUsingGrafanaDB(context.Background(), sc.loginUserQuery)
err := loginUsingGrafanaDB(context.Background(), sc.loginUserQuery, sc.store)
require.EqualError(t, err, ErrUserDisabled.Error())
assert.False(t, sc.validatePasswordCalled)
@@ -53,6 +53,7 @@ func TestLoginUsingGrafanaDB(t *testing.T) {
}
type grafanaLoginScenarioContext struct {
store *mockstore.SQLStoreMock
loginUserQuery *models.LoginUserQuery
validatePasswordCalled bool
}
@@ -66,6 +67,7 @@ func grafanaLoginScenario(t *testing.T, desc string, fn grafanaLoginScenarioFunc
origValidatePassword := validatePassword
sc := &grafanaLoginScenarioContext{
store: mockstore.NewSQLStoreMock(),
loginUserQuery: &models.LoginUserQuery{
Username: "user",
Password: "pwd",
@@ -95,14 +97,10 @@ func mockPasswordValidation(valid bool, sc *grafanaLoginScenarioContext) {
}
func (sc *grafanaLoginScenarioContext) getUserByLoginQueryReturns(user *models.User) {
bus.AddHandler("test", func(ctx context.Context, query *models.GetUserByLoginQuery) error {
if user == nil {
return models.ErrUserNotFound
}
query.Result = user
return nil
})
sc.store.ExpectedUser = user
if user == nil {
sc.store.ExpectedError = models.ErrUserNotFound
}
}
func (sc *grafanaLoginScenarioContext) withValidCredentials() {
+4 -43
View File
@@ -4,10 +4,10 @@ import (
"context"
"errors"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/ldap"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/multildap"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util/errutil"
@@ -27,7 +27,7 @@ var ldapLogger = log.New("login.ldap")
// loginUsingLDAP logs in user using LDAP. It returns whether LDAP is enabled and optional error and query arg will be
// populated with the logged in user if successful.
var loginUsingLDAP = func(ctx context.Context, query *models.LoginUserQuery) (bool, error) {
var loginUsingLDAP = func(ctx context.Context, query *models.LoginUserQuery, loginService login.Service) (bool, error) {
enabled := isLDAPEnabled()
if !enabled {
@@ -43,7 +43,7 @@ var loginUsingLDAP = func(ctx context.Context, query *models.LoginUserQuery) (bo
if err != nil {
if errors.Is(err, ldap.ErrCouldNotFindUser) {
// Ignore the error since user might not be present anyway
if err := DisableExternalUser(ctx, query.Username); err != nil {
if err := loginService.DisableExternalUser(ctx, query.Username); err != nil {
ldapLogger.Debug("Failed to disable external user", "err", err)
}
@@ -58,7 +58,7 @@ var loginUsingLDAP = func(ctx context.Context, query *models.LoginUserQuery) (bo
ExternalUser: externalUser,
SignupAllowed: setting.LDAPAllowSignup,
}
err = bus.Dispatch(ctx, upsert)
err = loginService.UpsertUser(ctx, upsert)
if err != nil {
return true, err
}
@@ -66,42 +66,3 @@ var loginUsingLDAP = func(ctx context.Context, query *models.LoginUserQuery) (bo
return true, nil
}
// DisableExternalUser marks external user as disabled in Grafana db
func DisableExternalUser(ctx context.Context, username string) error {
// Check if external user exist in Grafana
userQuery := &models.GetExternalUserInfoByLoginQuery{
LoginOrEmail: username,
}
if err := bus.Dispatch(ctx, userQuery); err != nil {
return err
}
userInfo := userQuery.Result
if !userInfo.IsDisabled {
ldapLogger.Debug(
"Disabling external user",
"user",
userQuery.Result.Login,
)
// Mark user as disabled in grafana db
disableUserCmd := &models.DisableUserCommand{
UserId: userQuery.Result.UserId,
IsDisabled: true,
}
if err := bus.Dispatch(ctx, disableUserCmd); err != nil {
ldapLogger.Debug(
"Error disabling external user",
"user",
userQuery.Result.Login,
"message",
err.Error(),
)
return err
}
}
return nil
}
+5 -2
View File
@@ -7,6 +7,7 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/ldap"
"github.com/grafana/grafana/pkg/services/login/logintest"
"github.com/grafana/grafana/pkg/services/multildap"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
@@ -28,7 +29,8 @@ func TestLoginUsingLDAP(t *testing.T) {
return config, nil
}
enabled, err := loginUsingLDAP(context.Background(), sc.loginUserQuery)
loginService := &logintest.LoginServiceFake{}
enabled, err := loginUsingLDAP(context.Background(), sc.loginUserQuery, loginService)
require.EqualError(t, err, errTest.Error())
assert.True(t, enabled)
@@ -39,7 +41,8 @@ func TestLoginUsingLDAP(t *testing.T) {
setting.LDAPEnabled = false
sc.withLoginResult(false)
enabled, err := loginUsingLDAP(context.Background(), sc.loginUserQuery)
loginService := &logintest.LoginServiceFake{}
enabled, err := loginUsingLDAP(context.Background(), sc.loginUserQuery, loginService)
require.NoError(t, err)
assert.False(t, enabled)
+5 -8
View File
@@ -5,7 +5,6 @@ import (
"testing"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -39,9 +38,7 @@ func TestMiddlewareAuth(t *testing.T) {
middlewareScenario(t, "ReqSignIn true and NoAnonynmous true", func(
t *testing.T, sc *scenarioContext) {
sqlStore := mockstore.NewSQLStoreMock()
sqlStore.ExpectedOrg = &models.Org{Id: orgID, Name: "test"}
sc.sqlStore = sqlStore
sc.mockSQLStore.ExpectedOrg = &models.Org{Id: orgID, Name: "test"}
sc.m.Get("/api/secure", ReqSignedInNoAnonymous, sc.defaultHandler)
sc.fakeReq("GET", "/api/secure").exec()
@@ -50,9 +47,7 @@ func TestMiddlewareAuth(t *testing.T) {
middlewareScenario(t, "ReqSignIn true and request with forceLogin in query string", func(
t *testing.T, sc *scenarioContext) {
sqlStore := mockstore.NewSQLStoreMock()
sqlStore.ExpectedOrg = &models.Org{Id: orgID, Name: "test"}
sc.sqlStore = sqlStore
sc.mockSQLStore.ExpectedOrg = &models.Org{Id: orgID, Name: "test"}
sc.m.Get("/secure", reqSignIn, sc.defaultHandler)
sc.fakeReq("GET", "/secure?forceLogin=true").exec()
@@ -65,7 +60,8 @@ func TestMiddlewareAuth(t *testing.T) {
middlewareScenario(t, "ReqSignIn true and request with same org provided in query string", func(
t *testing.T, sc *scenarioContext) {
org, err := sc.sqlStore.CreateOrgWithMember(sc.cfg.AnonymousOrgName, 1)
sc.mockSQLStore.ExpectedOrg = &models.Org{Id: 1, Name: sc.cfg.AnonymousOrgName}
org, err := sc.mockSQLStore.CreateOrgWithMember(sc.cfg.AnonymousOrgName, 1)
require.NoError(t, err)
sc.m.Get("/secure", reqSignIn, sc.defaultHandler)
@@ -77,6 +73,7 @@ func TestMiddlewareAuth(t *testing.T) {
middlewareScenario(t, "ReqSignIn true and request with different org provided in query string", func(
t *testing.T, sc *scenarioContext) {
sc.mockSQLStore.ExpectedOrg = &models.Org{Id: 1, Name: sc.cfg.AnonymousOrgName}
sc.m.Get("/secure", reqSignIn, sc.defaultHandler)
sc.fakeReq("GET", "/secure?orgId=2").exec()
+2 -1
View File
@@ -33,7 +33,7 @@ func Logger(cfg *setting.Cfg) web.Handler {
c.Next()
timeTaken := time.Since(start) / time.Millisecond
duration := time.Since(start).String()
ctx := contexthandler.FromContext(c.Req.Context())
if ctx != nil && ctx.PerfmonTimer != nil {
ctx.PerfmonTimer.Observe(float64(timeTaken))
@@ -53,6 +53,7 @@ func Logger(cfg *setting.Cfg) web.Handler {
"status", status,
"remote_addr", c.RemoteAddr(),
"time_ms", int64(timeTaken),
"duration", duration,
"size", rw.Size(),
"referer", req.Referer(),
}
+8 -27
View File
@@ -9,6 +9,7 @@ import (
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/contexthandler"
"github.com/grafana/grafana/pkg/services/login/logintest"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/stretchr/testify/assert"
@@ -28,10 +29,7 @@ func TestMiddlewareBasicAuth(t *testing.T) {
keyhash, err := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd")
require.NoError(t, err)
bus.AddHandler("test", func(ctx context.Context, query *models.GetApiKeyByNameQuery) error {
query.Result = &models.ApiKey{OrgId: orgID, Role: models.ROLE_EDITOR, Key: keyhash}
return nil
})
sc.mockSQLStore.ExpectedAPIKey = &models.ApiKey{OrgId: orgID, Role: models.ROLE_EDITOR, Key: keyhash}
authHeader := util.GetBasicAuthHeader("api_key", "eyJrIjoidjVuQXdwTWFmRlA2em5hUzR1cmhkV0RMUzU1MTFNNDIiLCJuIjoiYXNkIiwiaWQiOjF9")
sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).exec()
@@ -60,11 +58,7 @@ func TestMiddlewareBasicAuth(t *testing.T) {
return nil
})
bus.AddHandler("get-sign-user", func(ctx context.Context, query *models.GetSignedInUserQuery) error {
t.Log("Handling GetSignedInUserQuery")
query.Result = &models.SignedInUser{OrgId: orgID, UserId: id}
return nil
})
sc.mockSQLStore.ExpectedSignedInUser = &models.SignedInUser{OrgId: orgID, UserId: id}
authHeader := util.GetBasicAuthHeader("myUser", password)
sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).exec()
@@ -78,25 +72,12 @@ func TestMiddlewareBasicAuth(t *testing.T) {
const password = "MyPass"
const salt = "Salt"
login.Init()
encoded, err := util.EncodePassword(password, salt)
require.NoError(t, err)
bus.AddHandler("user-query", func(ctx context.Context, query *models.GetUserByLoginQuery) error {
encoded, err := util.EncodePassword(password, salt)
if err != nil {
return err
}
query.Result = &models.User{
Password: encoded,
Id: id,
Salt: salt,
}
return nil
})
bus.AddHandler("get-sign-user", func(ctx context.Context, query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{UserId: query.UserId}
return nil
})
sc.mockSQLStore.ExpectedUser = &models.User{Password: encoded, Id: id, Salt: salt}
sc.mockSQLStore.ExpectedSignedInUser = &models.SignedInUser{UserId: id}
login.ProvideService(sc.mockSQLStore, &logintest.LoginServiceFake{})
authHeader := util.GetBasicAuthHeader("myUser", password)
sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).exec()
+4 -27
View File
@@ -46,14 +46,7 @@ func TestMiddlewareJWTAuth(t *testing.T) {
"foo-username": myUsername,
}, nil
}
bus.AddHandler("get-sign-user", func(ctx context.Context, query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{
UserId: id,
OrgId: orgID,
Login: query.Login,
}
return nil
})
sc.mockSQLStore.ExpectedSignedInUser = &models.SignedInUser{UserId: id, OrgId: orgID, Login: myUsername}
sc.fakeReq("GET", "/").withJWTAuthHeader(token).exec()
assert.Equal(t, verifiedToken, token)
@@ -74,14 +67,7 @@ func TestMiddlewareJWTAuth(t *testing.T) {
"foo-email": myEmail,
}, nil
}
bus.AddHandler("get-sign-user", func(ctx context.Context, query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{
UserId: id,
OrgId: orgID,
Email: query.Email,
}
return nil
})
sc.mockSQLStore.ExpectedSignedInUser = &models.SignedInUser{UserId: id, OrgId: orgID, Email: myEmail}
sc.fakeReq("GET", "/").withJWTAuthHeader(token).exec()
assert.Equal(t, verifiedToken, token)
@@ -103,9 +89,7 @@ func TestMiddlewareJWTAuth(t *testing.T) {
"foo-email": myEmail,
}, nil
}
bus.AddHandler("get-sign-user", func(ctx context.Context, query *models.GetSignedInUserQuery) error {
return models.ErrUserNotFound
})
sc.mockSQLStore.ExpectedError = models.ErrUserNotFound
sc.fakeReq("GET", "/").withJWTAuthHeader(token).exec()
assert.Equal(t, verifiedToken, token)
@@ -124,14 +108,7 @@ func TestMiddlewareJWTAuth(t *testing.T) {
"foo-email": myEmail,
}, nil
}
bus.AddHandler("get-sign-user", func(ctx context.Context, query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{
UserId: id,
OrgId: orgID,
Email: query.Email,
}
return nil
})
sc.mockSQLStore.ExpectedSignedInUser = &models.SignedInUser{UserId: id, OrgId: orgID, Email: myEmail}
bus.AddHandler("upsert-user", func(ctx context.Context, command *models.UpsertUserCommand) error {
command.Result = &models.User{
Id: id,
+9 -30
View File
@@ -26,7 +26,6 @@ import (
"github.com/grafana/grafana/pkg/services/contexthandler/authproxy"
"github.com/grafana/grafana/pkg/services/login/loginservice"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
@@ -150,10 +149,7 @@ func TestMiddlewareContext(t *testing.T) {
keyhash, err := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd")
require.NoError(t, err)
bus.AddHandler("test", func(ctx context.Context, query *models.GetApiKeyByNameQuery) error {
query.Result = &models.ApiKey{OrgId: orgID, Role: models.ROLE_EDITOR, Key: keyhash}
return nil
})
sc.mockSQLStore.ExpectedAPIKey = &models.ApiKey{OrgId: orgID, Role: models.ROLE_EDITOR, Key: keyhash}
sc.fakeReq("GET", "/").withValidApiKey().exec()
@@ -166,11 +162,7 @@ func TestMiddlewareContext(t *testing.T) {
middlewareScenario(t, "Valid API key, but does not match DB hash", func(t *testing.T, sc *scenarioContext) {
const keyhash = "Something_not_matching"
bus.AddHandler("test", func(ctx context.Context, query *models.GetApiKeyByNameQuery) error {
query.Result = &models.ApiKey{OrgId: 12, Role: models.ROLE_EDITOR, Key: keyhash}
return nil
})
sc.mockSQLStore.ExpectedAPIKey = &models.ApiKey{OrgId: 12, Role: models.ROLE_EDITOR, Key: keyhash}
sc.fakeReq("GET", "/").withValidApiKey().exec()
@@ -184,13 +176,8 @@ func TestMiddlewareContext(t *testing.T) {
keyhash, err := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd")
require.NoError(t, err)
bus.AddHandler("test", func(ctx context.Context, query *models.GetApiKeyByNameQuery) error {
// api key expired one second before
expires := sc.contextHandler.GetTime().Add(-1 * time.Second).Unix()
query.Result = &models.ApiKey{OrgId: 12, Role: models.ROLE_EDITOR, Key: keyhash,
Expires: &expires}
return nil
})
expires := sc.contextHandler.GetTime().Add(-1 * time.Second).Unix()
sc.mockSQLStore.ExpectedAPIKey = &models.ApiKey{OrgId: 12, Role: models.ROLE_EDITOR, Key: keyhash, Expires: &expires}
sc.fakeReq("GET", "/").withValidApiKey().exec()
@@ -203,11 +190,7 @@ func TestMiddlewareContext(t *testing.T) {
const userID int64 = 12
sc.withTokenSessionCookie("token")
bus.AddHandler("test", func(ctx context.Context, query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{OrgId: 2, UserId: userID}
return nil
})
sc.mockSQLStore.ExpectedSignedInUser = &models.SignedInUser{OrgId: 2, UserId: userID}
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) {
return &models.UserToken{
@@ -231,11 +214,7 @@ func TestMiddlewareContext(t *testing.T) {
const userID int64 = 12
sc.withTokenSessionCookie("token")
bus.AddHandler("test", func(ctx context.Context, query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{OrgId: 2, UserId: userID}
return nil
})
sc.mockSQLStore.ExpectedSignedInUser = &models.SignedInUser{OrgId: 2, UserId: userID}
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) {
return &models.UserToken{
@@ -332,7 +311,8 @@ func TestMiddlewareContext(t *testing.T) {
})
middlewareScenario(t, "When anonymous access is enabled", func(t *testing.T, sc *scenarioContext) {
org, err := sc.sqlStore.CreateOrgWithMember(sc.cfg.AnonymousOrgName, 1)
sc.mockSQLStore.ExpectedOrg = &models.Org{Id: 1, Name: sc.cfg.AnonymousOrgName}
org, err := sc.mockSQLStore.CreateOrgWithMember(sc.cfg.AnonymousOrgName, 1)
require.NoError(t, err)
sc.fakeReq("GET", "/").exec()
@@ -651,7 +631,6 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc, cbs ...func(
func getContextHandler(t *testing.T, cfg *setting.Cfg, mockSQLStore *mockstore.SQLStoreMock, loginService *loginservice.LoginServiceMock) *contexthandler.ContextHandler {
t.Helper()
sqlStore := sqlstore.InitTestDB(t)
if cfg == nil {
cfg = setting.NewCfg()
}
@@ -666,7 +645,7 @@ func getContextHandler(t *testing.T, cfg *setting.Cfg, mockSQLStore *mockstore.S
tracer, err := tracing.InitializeTracerForTest()
authProxy := authproxy.ProvideAuthProxy(cfg, remoteCacheSvc, loginService, mockSQLStore)
require.NoError(t, err)
return contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, sqlStore, tracer, authProxy)
return contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, mockSQLStore, tracer, authProxy)
}
type fakeRenderService struct {
+2 -10
View File
@@ -46,15 +46,11 @@ func TestOrgRedirectMiddleware(t *testing.T) {
for _, tc := range testCases {
middlewareScenario(t, tc.desc, func(t *testing.T, sc *scenarioContext) {
sc.withTokenSessionCookie("token")
sc.mockSQLStore.ExpectedSignedInUser = &models.SignedInUser{OrgId: 1, UserId: 12}
bus.AddHandler("test", func(ctx context.Context, query *models.SetUsingOrgCommand) error {
return nil
})
bus.AddHandler("test", func(ctx context.Context, query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{OrgId: 1, UserId: 12}
return nil
})
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) {
return &models.UserToken{
UserId: 0,
@@ -75,11 +71,7 @@ func TestOrgRedirectMiddleware(t *testing.T) {
bus.AddHandler("test", func(ctx context.Context, query *models.SetUsingOrgCommand) error {
return fmt.Errorf("")
})
bus.AddHandler("test", func(ctx context.Context, query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{OrgId: 1, UserId: 12}
return nil
})
sc.mockSQLStore.ExpectedSignedInUser = &models.SignedInUser{OrgId: 1, UserId: 12}
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) {
return &models.UserToken{
+1
View File
@@ -60,6 +60,7 @@ func TestMiddlewareQuota(t *testing.T) {
const quotaUsed = 4
setUp := func(sc *scenarioContext) {
sc.withTokenSessionCookie("token")
sc.mockSQLStore.ExpectedSignedInUser = &models.SignedInUser{UserId: 12}
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) {
return &models.UserToken{
UserId: 12,
+11
View File
@@ -49,6 +49,17 @@ func (r RoleType) Children() []RoleType {
}
}
func (r RoleType) Parents() []RoleType {
switch r {
case ROLE_EDITOR:
return []RoleType{ROLE_ADMIN}
case ROLE_VIEWER:
return []RoleType{ROLE_EDITOR, ROLE_ADMIN}
default:
return nil
}
}
func (r *RoleType) UnmarshalJSON(data []byte) error {
var str string
err := json.Unmarshal(data, &str)
+17 -15
View File
@@ -169,20 +169,22 @@ type GetUserOrgListQuery struct {
// DTO & Projections
type SignedInUser struct {
UserId int64
OrgId int64
OrgName string
OrgRole RoleType
Login string
Name string
Email string
ApiKeyId int64
OrgCount int
IsGrafanaAdmin bool
IsAnonymous bool
HelpFlags1 HelpFlags1
LastSeenAt time.Time
Teams []int64
UserId int64
OrgId int64
OrgName string
OrgRole RoleType
ExternalAuthModule string
ExternalAuthId string
Login string
Name string
Email string
ApiKeyId int64
OrgCount int
IsGrafanaAdmin bool
IsAnonymous bool
HelpFlags1 HelpFlags1
LastSeenAt time.Time
Teams []int64
// Permissions grouped by orgID and actions
Permissions map[int64]map[string][]string `json:"-"`
}
@@ -231,7 +233,7 @@ type UserProfileDTO struct {
Name string `json:"name"`
Login string `json:"login"`
Theme string `json:"theme"`
OrgId int64 `json:"orgId"`
OrgId int64 `json:"orgId,omitempty"`
IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
IsDisabled bool `json:"isDisabled"`
IsExternal bool `json:"isExternal"`
+7 -1
View File
@@ -271,7 +271,13 @@ func setDefaultNavURL(p *plugins.Plugin) {
p.DefaultNavURL = path.Join("/plugins/", p.ID, "/page/", include.Slug)
}
if include.Type == "dashboard" {
p.DefaultNavURL = path.Join("/dashboard/db/", include.Slug)
dboardURL := include.DashboardURLPath()
if dboardURL == "" {
p.Logger().Warn("Included dashboard is missing a UID field")
continue
}
p.DefaultNavURL = dboardURL
}
}
}
+64 -8
View File
@@ -378,7 +378,7 @@ func TestLoader_Load(t *testing.T) {
Plugins: []plugins.Dependency{},
},
Includes: []*plugins.Includes{
{Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: "Viewer", Slug: "nginx-memory", DefaultNav: true},
{Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: "Viewer", Slug: "nginx-memory"},
{Name: "Root Page (react)", Type: "page", Role: "Viewer", Path: "/a/my-simple-app", DefaultNav: true, AddToNav: true, Slug: "root-page-react"},
},
Backend: false,
@@ -411,6 +411,60 @@ func TestLoader_Load(t *testing.T) {
}
}
func TestLoader_setDefaultNavURL(t *testing.T) {
t.Run("When including a dashboard with DefaultNav: true", func(t *testing.T) {
pluginWithDashboard := &plugins.Plugin{
JSONData: plugins.JSONData{Includes: []*plugins.Includes{
{
Type: "dashboard",
DefaultNav: true,
UID: "",
},
}},
}
logger := &fakeLogger{loggedLines: []string{}}
pluginWithDashboard.SetLogger(logger)
t.Run("Default nav URL is not set if dashboard UID field not is set", func(t *testing.T) {
setDefaultNavURL(pluginWithDashboard)
require.Equal(t, "", pluginWithDashboard.DefaultNavURL)
require.Equal(t, []string{"Included dashboard is missing a UID field"}, logger.loggedLines)
})
t.Run("Default nav URL is set if dashboard UID field is set", func(t *testing.T) {
pluginWithDashboard.Includes[0].UID = "a1b2c3"
setDefaultNavURL(pluginWithDashboard)
require.Equal(t, "/d/a1b2c3", pluginWithDashboard.DefaultNavURL)
})
})
t.Run("When including a page with DefaultNav: true", func(t *testing.T) {
pluginWithPage := &plugins.Plugin{
JSONData: plugins.JSONData{Includes: []*plugins.Includes{
{
Type: "page",
DefaultNav: true,
Slug: "testPage",
},
}},
}
t.Run("Default nav URL is set using slug", func(t *testing.T) {
setDefaultNavURL(pluginWithPage)
require.Equal(t, "/plugins/page/testPage", pluginWithPage.DefaultNavURL)
})
t.Run("Default nav URL is set using slugified Name field if Slug field is empty", func(t *testing.T) {
pluginWithPage.Includes[0].Slug = ""
pluginWithPage.Includes[0].Name = "My Test Page"
setDefaultNavURL(pluginWithPage)
require.Equal(t, "/plugins/page/my-test-page", pluginWithPage.DefaultNavURL)
})
})
}
func TestLoader_Load_MultiplePlugins(t *testing.T) {
parentDir, err := filepath.Abs("../")
if err != nil {
@@ -1126,20 +1180,22 @@ func (*fakeLicensingService) FeatureEnabled(feature string) bool {
type fakeLogger struct {
log.Logger
loggedLines []string
}
func (fl fakeLogger) New(_ ...interface{}) *log.ConcreteLogger {
func (fl *fakeLogger) New(_ ...interface{}) *log.ConcreteLogger {
return &log.ConcreteLogger{}
}
func (fl fakeLogger) Info(_ string, _ ...interface{}) {
func (fl *fakeLogger) Info(l string, _ ...interface{}) {
fl.loggedLines = append(fl.loggedLines, l)
}
func (fl fakeLogger) Debug(_ string, _ ...interface{}) {
func (fl *fakeLogger) Debug(l string, _ ...interface{}) {
fl.loggedLines = append(fl.loggedLines, l)
}
func (fl fakeLogger) Warn(_ string, _ ...interface{}) {
func (fl *fakeLogger) Warn(l string, _ ...interface{}) {
fl.loggedLines = append(fl.loggedLines, l)
}
@@ -20,8 +20,7 @@
{
"type": "dashboard",
"name": "Nginx Memory",
"path": "dashboards/memory.json",
"defaultNav": true
"path": "dashboards/memory.json"
},
{
"type": "page",
+4 -5
View File
@@ -97,12 +97,11 @@ type Includes struct {
ID string `json:"-"`
}
func (e Includes) GetSlugOrUIDLink() string {
if len(e.UID) > 0 {
return "/d/" + e.UID
} else {
return "/dashboard/db/" + e.Slug
func (e Includes) DashboardURLPath() string {
if e.Type != "dashboard" || len(e.UID) == 0 {
return ""
}
return "/d/" + e.UID
}
type Dependency struct {
+1 -1
View File
@@ -118,7 +118,7 @@ func (s *Server) init() error {
return err
}
login.Init()
login.ProvideService(s.HTTPServer.SQLStore, s.HTTPServer.Login)
social.ProvideService(s.cfg)
if err := s.roleRegistry.RegisterFixedRoles(); err != nil {
+6 -1
View File
@@ -7,6 +7,7 @@ import (
"github.com/google/wire"
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/api"
"github.com/grafana/grafana/pkg/api/avatar"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/expr"
@@ -20,6 +21,7 @@ import (
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/infra/usagestats"
uss "github.com/grafana/grafana/pkg/infra/usagestats/service"
loginpkg "github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
@@ -160,6 +162,8 @@ var wireBasicSet = wire.NewSet(
wire.Bind(new(login.AuthInfoService), new(*authinfoservice.Implementation)),
authinfodatabase.ProvideAuthInfoStore,
wire.Bind(new(login.Store), new(*authinfodatabase.AuthInfoStore)),
loginpkg.ProvideService,
wire.Bind(new(loginpkg.Authenticator), new(*loginpkg.AuthenticatorService)),
datasourceproxy.ProvideService,
search.ProvideService,
searchV2.ProvideService,
@@ -231,6 +235,7 @@ var wireBasicSet = wire.NewSet(
comments.ProvideService,
guardian.ProvideService,
secretsStore.ProvideService,
avatar.ProvideAvatarCacheServer,
authproxy.ProvideAuthProxy,
)
@@ -260,7 +265,7 @@ var wireTestSet = wire.NewSet(
wire.Bind(new(notifications.WebhookSender), new(*notifications.NotificationServiceMock)),
wire.Bind(new(notifications.EmailSender), new(*notifications.NotificationServiceMock)),
mockstore.NewSQLStoreMock,
wire.Bind(new(sqlstore.Store), new(*mockstore.SQLStoreMock)),
wire.Bind(new(sqlstore.Store), new(*sqlstore.SQLStore)),
)
func Initialize(cla setting.CommandLineArgs, opts Options, apiOpts api.ServerOptions) (*Server, error) {
@@ -16,38 +16,81 @@ import (
)
func ProvideService(features featuremgmt.FeatureToggles, usageStats usagestats.Service,
provider accesscontrol.PermissionsProvider, routeRegister routing.RouteRegister) *OSSAccessControlService {
s := ProvideOSSAccessControl(features, usageStats, provider)
s.registerUsageMetrics()
provider accesscontrol.PermissionsProvider, routeRegister routing.RouteRegister) (*OSSAccessControlService, error) {
var errDeclareRoles error
s := ProvideOSSAccessControl(features, provider)
s.registerUsageMetrics(usageStats)
if !s.IsDisabled() {
api := api.AccessControlAPI{
RouteRegister: routeRegister,
AccessControl: s,
}
api.RegisterAPIEndpoints()
errDeclareRoles = accesscontrol.DeclareFixedRoles(s)
}
return s, errDeclareRoles
}
func macroRoles() map[string]*accesscontrol.RoleDTO {
return map[string]*accesscontrol.RoleDTO{
string(models.ROLE_ADMIN): {
Name: "fixed:builtins:admin",
DisplayName: string(models.ROLE_ADMIN),
Description: "Admin role",
Group: "Basic",
Version: 1,
Permissions: []accesscontrol.Permission{},
},
string(models.ROLE_EDITOR): {
Name: "fixed:builtins:editor",
DisplayName: string(models.ROLE_EDITOR),
Description: "Editor role",
Group: "Basic",
Version: 1,
Permissions: []accesscontrol.Permission{},
},
string(models.ROLE_VIEWER): {
Name: "fixed:builtins:viewer",
DisplayName: string(models.ROLE_VIEWER),
Description: "Viewer role",
Group: "Basic",
Version: 1,
Permissions: []accesscontrol.Permission{},
},
accesscontrol.RoleGrafanaAdmin: {
Name: "fixed:builtins:grafana_admin",
DisplayName: accesscontrol.RoleGrafanaAdmin,
Description: "Grafana Admin role",
Group: "Basic",
Version: 1,
Permissions: []accesscontrol.Permission{},
},
}
return s
}
// ProvideOSSAccessControl creates an oss implementation of access control without usage stats registration
func ProvideOSSAccessControl(features featuremgmt.FeatureToggles, usageStats usagestats.Service, provider accesscontrol.PermissionsProvider) *OSSAccessControlService {
return &OSSAccessControlService{
func ProvideOSSAccessControl(features featuremgmt.FeatureToggles, provider accesscontrol.PermissionsProvider) *OSSAccessControlService {
s := &OSSAccessControlService{
features: features,
provider: provider,
usageStats: usageStats,
log: log.New("accesscontrol"),
scopeResolver: accesscontrol.NewScopeResolver(),
roles: macroRoles(),
}
return s
}
// OSSAccessControlService is the service implementing role based access control.
type OSSAccessControlService struct {
log log.Logger
usageStats usagestats.Service
features featuremgmt.FeatureToggles
scopeResolver accesscontrol.ScopeResolver
provider accesscontrol.PermissionsProvider
registrations accesscontrol.RegistrationList
roles map[string]*accesscontrol.RoleDTO
}
func (ac *OSSAccessControlService) IsDisabled() bool {
@@ -57,8 +100,8 @@ func (ac *OSSAccessControlService) IsDisabled() bool {
return !ac.features.IsEnabled(featuremgmt.FlagAccesscontrol)
}
func (ac *OSSAccessControlService) registerUsageMetrics() {
ac.usageStats.RegisterMetricsFunc(func(context.Context) (map[string]interface{}, error) {
func (ac *OSSAccessControlService) registerUsageMetrics(usageStats usagestats.Service) {
usageStats.RegisterMetricsFunc(func(context.Context) (map[string]interface{}, error) {
return map[string]interface{}{
"stats.oss.accesscontrol.enabled.count": ac.getUsageMetrics(),
}, nil
@@ -140,15 +183,9 @@ func (ac *OSSAccessControlService) getFixedPermissions(ctx context.Context, user
permissions := make([]*accesscontrol.Permission, 0)
for _, builtin := range ac.GetUserBuiltInRoles(user) {
if roleNames, ok := accesscontrol.FixedRoleGrants[builtin]; ok {
for _, name := range roleNames {
role, exists := accesscontrol.FixedRoles[name]
if !exists {
continue
}
for i := range role.Permissions {
permissions = append(permissions, &role.Permissions[i])
}
if macroRole, ok := ac.roles[builtin]; ok {
for i := range macroRole.Permissions {
permissions = append(permissions, &macroRole.Permissions[i])
}
}
}
@@ -157,49 +194,20 @@ func (ac *OSSAccessControlService) getFixedPermissions(ctx context.Context, user
}
func (ac *OSSAccessControlService) GetUserBuiltInRoles(user *models.SignedInUser) []string {
roles := []string{string(user.OrgRole)}
for _, role := range user.OrgRole.Children() {
roles = append(roles, string(role))
builtInRoles := []string{string(user.OrgRole)}
// With built-in role simplifying, inheritance is performed upon role registration.
if !ac.features.IsEnabled(featuremgmt.FlagAccesscontrolBuiltins) {
for _, br := range user.OrgRole.Children() {
builtInRoles = append(builtInRoles, string(br))
}
}
if user.IsGrafanaAdmin {
roles = append(roles, accesscontrol.RoleGrafanaAdmin)
builtInRoles = append(builtInRoles, accesscontrol.RoleGrafanaAdmin)
}
return roles
}
func (ac *OSSAccessControlService) saveFixedRole(role accesscontrol.RoleDTO) {
if storedRole, ok := accesscontrol.FixedRoles[role.Name]; ok {
// If a package wants to override another package's role, the version
// needs to be increased. Hence, we don't overwrite a role with a
// greater version.
if storedRole.Version >= role.Version {
ac.log.Debug("the role has already been stored in a greater version, skipping registration", "role", role.Name)
return
}
}
// Save role
accesscontrol.FixedRoles[role.Name] = role
}
func (ac *OSSAccessControlService) assignFixedRole(role accesscontrol.RoleDTO, builtInRoles []string) {
for _, builtInRole := range builtInRoles {
// Only record new assignments
alreadyAssigned := false
assignments, ok := accesscontrol.FixedRoleGrants[builtInRole]
if ok {
for _, assignedRole := range assignments {
if assignedRole == role.Name {
ac.log.Debug("the role has already been assigned", "rolename", role.Name, "build_in_role", builtInRole)
alreadyAssigned = true
}
}
}
if !alreadyAssigned {
assignments = append(assignments, role.Name)
accesscontrol.FixedRoleGrants[builtInRole] = assignments
}
}
return builtInRoles
}
// RegisterFixedRoles registers all declared roles in RAM
@@ -208,18 +216,33 @@ func (ac *OSSAccessControlService) RegisterFixedRoles() error {
if ac.IsDisabled() {
return nil
}
var err error
ac.registrations.Range(func(registration accesscontrol.RoleRegistration) bool {
ac.registerFixedRole(registration.Role, registration.Grants)
return true
})
return err
return nil
}
// RegisterFixedRole saves a fixed role and assigns it to built-in roles
func (ac *OSSAccessControlService) registerFixedRole(role accesscontrol.RoleDTO, builtInRoles []string) {
ac.saveFixedRole(role)
ac.assignFixedRole(role, builtInRoles)
// Inheritance
brs := map[string]struct{}{}
for _, builtInRole := range builtInRoles {
brs[builtInRole] = struct{}{}
if builtInRole != accesscontrol.RoleGrafanaAdmin {
for _, parent := range models.RoleType(builtInRole).Parents() {
brs[string(parent)] = struct{}{}
}
}
}
for br := range brs {
if macroRole, ok := ac.roles[br]; ok {
macroRole.Permissions = append(macroRole.Permissions, role.Permissions...)
} else {
ac.log.Error("Unknown builtin role", "builtInRole", br)
}
}
}
// DeclareFixedRoles allow the caller to declare, to the service, fixed roles and their assignments
@@ -2,7 +2,6 @@ package ossaccesscontrol
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
@@ -23,37 +22,16 @@ func setupTestEnv(t testing.TB) *OSSAccessControlService {
ac := &OSSAccessControlService{
features: featuremgmt.WithFeatures(featuremgmt.FlagAccesscontrol),
usageStats: &usagestats.UsageStatsMock{T: t},
log: log.New("accesscontrol"),
registrations: accesscontrol.RegistrationList{},
scopeResolver: accesscontrol.NewScopeResolver(),
provider: database.ProvideService(sqlstore.InitTestDB(t)),
roles: macroRoles(),
}
require.NoError(t, ac.RegisterFixedRoles())
return ac
}
func removeRoleHelper(role string) {
delete(accesscontrol.FixedRoles, role)
// Compute new grants removing any appearance of the role in the list
replaceGrants := map[string][]string{}
for builtInRole, grants := range accesscontrol.FixedRoleGrants {
newGrants := make([]string, len(grants))
for _, r := range grants {
if r != role {
newGrants = append(newGrants, r)
}
}
replaceGrants[builtInRole] = newGrants
}
// Replace grants
for br, grants := range replaceGrants {
accesscontrol.FixedRoleGrants[br] = grants
}
}
// extractRawPermissionsHelper extracts action and scope fields only from a permission slice
func extractRawPermissionsHelper(perms []*accesscontrol.Permission) []*accesscontrol.Permission {
res := make([]*accesscontrol.Permission, len(perms))
@@ -112,6 +90,13 @@ func TestEvaluatingPermissions(t *testing.T) {
t.Run(tc.desc, func(t *testing.T) {
ac := setupTestEnv(t)
// Use OSS roles for this test to pass
err := accesscontrol.DeclareFixedRoles(ac)
require.NoError(t, err)
errRegisterRoles := ac.RegisterFixedRoles()
require.NoError(t, errRegisterRoles)
user := &models.SignedInUser{
UserId: 1,
OrgId: 1,
@@ -149,13 +134,16 @@ func TestUsageMetrics(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := ProvideService(
usagestatsmock := &usagestats.UsageStatsMock{T: t}
_, errInitAc := ProvideService(
featuremgmt.WithFeatures("accesscontrol", tt.enabled),
&usagestats.UsageStatsMock{T: t},
usagestatsmock,
database.ProvideService(sqlstore.InitTestDB(t)),
routing.NewRouteRegister(),
)
report, err := s.usageStats.GetUsageReport(context.Background())
require.NoError(t, errInitAc)
report, err := usagestatsmock.GetUsageReport(context.Background())
assert.Nil(t, err)
assert.Equal(t, tt.expectedValue, report.Metrics["stats.oss.accesscontrol.enabled.count"])
@@ -163,151 +151,35 @@ func TestUsageMetrics(t *testing.T) {
}
}
type assignmentTestCase struct {
role accesscontrol.RoleDTO
builtInRoles []string
}
func TestOSSAccessControlService_RegisterFixedRole(t *testing.T) {
tests := []struct {
name string
runs []assignmentTestCase
}{
{
name: "Successfully register role no assignments",
runs: []assignmentTestCase{
{
role: accesscontrol.RoleDTO{
Version: 1,
Name: "fixed:test:test",
},
},
},
},
{
name: "Successfully ignore overwriting existing role",
runs: []assignmentTestCase{
{
role: accesscontrol.RoleDTO{
Version: 1,
Name: "fixed:test:test",
},
},
{
role: accesscontrol.RoleDTO{
Version: 1,
Name: "fixed:test:test",
},
},
},
},
{
name: "Successfully register and assign role",
runs: []assignmentTestCase{
{
role: accesscontrol.RoleDTO{
Version: 1,
Name: "fixed:test:test",
},
builtInRoles: []string{"Viewer", "Editor", "Admin"},
},
},
},
{
name: "Successfully ignore unchanged assignment",
runs: []assignmentTestCase{
{
role: accesscontrol.RoleDTO{
Version: 1,
Name: "fixed:test:test",
},
builtInRoles: []string{"Viewer"},
},
{
role: accesscontrol.RoleDTO{
Version: 2,
Name: "fixed:test:test",
},
builtInRoles: []string{"Viewer"},
},
},
},
{
name: "Successfully add a new assignment",
runs: []assignmentTestCase{
{
role: accesscontrol.RoleDTO{
Version: 1,
Name: "fixed:test:test",
},
builtInRoles: []string{"Viewer"},
},
{
role: accesscontrol.RoleDTO{
Version: 1,
Name: "fixed:test:test",
},
builtInRoles: []string{"Editor"},
},
},
},
perm := accesscontrol.Permission{Action: "test:test", Scope: "test:*"}
role := accesscontrol.RoleDTO{
Version: 1,
Name: "fixed:test:test",
Permissions: []accesscontrol.Permission{perm},
}
builtInRoles := []string{"Editor"}
// Admin is going to get the role as well
includedBuiltInRoles := []string{"Editor", "Admin"}
// Grafana Admin and Viewer won't get the role
excludedbuiltInRoles := []string{"Viewer", "Grafana Admin"}
ac := setupTestEnv(t)
ac.registerFixedRole(role, builtInRoles)
for _, br := range includedBuiltInRoles {
builtinRole, ok := ac.roles[br]
assert.True(t, ok)
assert.Contains(t, builtinRole.Permissions, perm)
}
// Check all runs performed so far to get the number of assignments seeder
// should have recorded
getTotalAssignCount := func(curRunIdx int, runs []assignmentTestCase) int {
builtIns := map[string]struct{}{}
for i := 0; i < curRunIdx+1; i++ {
for _, br := range runs[i].builtInRoles {
builtIns[br] = struct{}{}
}
}
return len(builtIns)
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
ac := &OSSAccessControlService{
features: featuremgmt.WithFeatures(),
usageStats: &usagestats.UsageStatsMock{T: t},
log: log.New("accesscontrol-test"),
}
for i, run := range tc.runs {
// Remove any inserted role after the test case has been run
t.Cleanup(func() { removeRoleHelper(run.role.Name) })
ac.registerFixedRole(run.role, run.builtInRoles)
// Check role has been registered
storedRole, ok := accesscontrol.FixedRoles[run.role.Name]
assert.True(t, ok, "role should have been registered")
// Check registered role has not been altered
assert.Equal(t, run.role, storedRole, "role should not have been altered")
// Check assignments
// Count number of times the role has been assigned
assignCnt := 0
for _, grants := range accesscontrol.FixedRoleGrants {
for _, r := range grants {
if r == run.role.Name {
assignCnt++
}
}
}
assert.Equal(t, getTotalAssignCount(i, tc.runs), assignCnt,
"assignments should only be added, never removed")
for _, br := range run.builtInRoles {
assigns, ok := accesscontrol.FixedRoleGrants[br]
assert.True(t, ok,
fmt.Sprintf("role %s should have been assigned to %s", run.role.Name, br))
assert.Contains(t, assigns, run.role.Name,
fmt.Sprintf("role %s should have been assigned to %s", run.role.Name, br))
}
}
})
for _, br := range excludedbuiltInRoles {
builtinRole, ok := ac.roles[br]
assert.True(t, ok)
assert.NotContains(t, builtinRole.Permissions, perm)
}
}
@@ -388,6 +260,9 @@ func TestOSSAccessControlService_DeclareFixedRoles(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
ac := setupTestEnv(t)
// Reset the registations
ac.registrations = accesscontrol.RegistrationList{}
// Test
err := ac.DeclareFixedRoles(tt.registrations...)
if tt.wantErr {
@@ -423,10 +298,11 @@ func TestOSSAccessControlService_RegisterFixedRoles(t *testing.T) {
registrations: []accesscontrol.RoleRegistration{
{
Role: accesscontrol.RoleDTO{
Version: 1,
Name: "fixed:test:test",
Version: 1,
Name: "fixed:test:test",
Permissions: []accesscontrol.Permission{{Action: "test:test"}},
},
Grants: []string{"Admin"},
Grants: []string{"Editor"},
},
},
wantErr: false,
@@ -436,17 +312,22 @@ func TestOSSAccessControlService_RegisterFixedRoles(t *testing.T) {
registrations: []accesscontrol.RoleRegistration{
{
Role: accesscontrol.RoleDTO{
Version: 1,
Name: "fixed:test:test",
Version: 1,
Name: "fixed:test:test",
Permissions: []accesscontrol.Permission{{Action: "test:test"}},
},
Grants: []string{"Admin"},
Grants: []string{"Editor"},
},
{
Role: accesscontrol.RoleDTO{
Version: 1,
Name: "fixed:test2:test2",
Permissions: []accesscontrol.Permission{
{Action: "test:test2"},
{Action: "test:test3", Scope: "test:*"},
},
},
Grants: []string{"Admin"},
Grants: []string{"Viewer"},
},
},
wantErr: false,
@@ -455,13 +336,8 @@ func TestOSSAccessControlService_RegisterFixedRoles(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Remove any inserted role after the test case has been run
t.Cleanup(func() {
for _, registration := range tt.registrations {
removeRoleHelper(registration.Role.Name)
}
})
ac := setupTestEnv(t)
ac.registrations.Append(tt.registrations...)
// Test
@@ -474,18 +350,24 @@ func TestOSSAccessControlService_RegisterFixedRoles(t *testing.T) {
// Check
for _, registration := range tt.registrations {
role, ok := accesscontrol.FixedRoles[registration.Role.Name]
assert.True(t, ok,
fmt.Sprintf("role %s should have been registered", registration.Role.Name))
assert.NotNil(t, role,
fmt.Sprintf("role %s should have been registered", registration.Role.Name))
// Prepare list of builtin roles to check
brAndParents := map[string]struct{}{}
for _, br := range registration.Grants {
rolesWithGrant, ok := accesscontrol.FixedRoleGrants[br]
assert.True(t, ok,
fmt.Sprintf("role %s should have been assigned to %s", registration.Role.Name, br))
assert.Contains(t, rolesWithGrant, registration.Role.Name,
fmt.Sprintf("role %s should have been assigned to %s", registration.Role.Name, br))
brAndParents[br] = struct{}{}
if br != accesscontrol.RoleGrafanaAdmin {
for _, parent := range models.RoleType(br).Parents() {
brAndParents[string(parent)] = struct{}{}
}
}
}
// Check builtin roles (parents included) have been granted with the permissions
for br := range brAndParents {
builtinRole, ok := ac.roles[br]
assert.True(t, ok)
for _, expectedPermission := range registration.Role.Permissions {
assert.Contains(t, builtinRole.Permissions, expectedPermission)
}
}
}
})
@@ -529,11 +411,6 @@ func TestOSSAccessControlService_GetUserPermissions(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Remove any inserted role after the test case has been run
t.Cleanup(func() {
removeRoleHelper(registration.Role.Name)
})
// Setup
ac := setupTestEnv(t)
@@ -614,11 +491,6 @@ func TestOSSAccessControlService_Evaluate(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Remove any inserted role after the test case has been run
t.Cleanup(func() {
removeRoleHelper(registration.Role.Name)
})
// Setup
ac := setupTestEnv(t)
ac.RegisterAttributeScopeResolver("users:login:", userLoginScopeSolver)
@@ -169,13 +169,18 @@ func ProvideDashboardPermissions(
return nil
},
InheritedScopePrefixes: []string{"folders:uid:"},
InheritedScopesSolver: func(ctx context.Context, orgID int64, resourceID string) ([]string, error) {
dashboard, err := getDashboard(ctx, orgID, resourceID)
if err != nil {
return nil, err
}
if dashboard.FolderId > 0 {
return []string{dashboards.ScopeFoldersProvider.GetResourceScopeUID(dashboard.Uid)}, nil
query := &models.GetDashboardQuery{Id: dashboard.FolderId, OrgId: orgID}
if err := sql.GetDashboard(ctx, query); err != nil {
return nil, err
}
return []string{dashboards.ScopeFoldersProvider.GetResourceScopeUID(query.Result.Uid)}, nil
}
return []string{}, nil
},
@@ -30,18 +30,45 @@ func newApi(ac accesscontrol.AccessControl, router routing.RouteRegister, manage
return &api{ac, router, manager, permissions}
}
func (a *api) getEvaluators(actionRead, actionWrite, scope string) (read, write accesscontrol.Evaluator) {
if a.service.options.InheritedScopesSolver == nil || a.service.options.InheritedScopePrefixes == nil {
read = accesscontrol.EvalPermission(actionRead, scope)
write = accesscontrol.EvalPermission(actionWrite, scope)
} else {
// Add inherited scopes to the evaluators protecting the endpoint.
// Scopes in the request context parameters are to be added by solveInheritedScopes.
// If a user got actionRead on any of the inherited scopes, they will be granted access to the endpoint.
// Ex: a user inherits dashboards:read from the containing folder (folders:uid:BCeknZL7k)
inheritedRead := []accesscontrol.Evaluator{accesscontrol.EvalPermission(actionRead, scope)}
inheritedWrite := []accesscontrol.Evaluator{accesscontrol.EvalPermission(actionWrite, scope)}
for _, scopePrefix := range a.service.options.InheritedScopePrefixes {
inheritedRead = append(inheritedRead,
accesscontrol.EvalPermission(actionRead, accesscontrol.Parameter(scopePrefix)))
inheritedWrite = append(inheritedWrite,
accesscontrol.EvalPermission(actionWrite, accesscontrol.Parameter(scopePrefix)))
}
read = accesscontrol.EvalAny(inheritedRead...)
write = accesscontrol.EvalAny(inheritedWrite...)
}
return
}
func (a *api) registerEndpoints() {
auth := middleware.Middleware(a.ac)
uidSolver := solveUID(a.service.options.UidSolver)
inheritanceSolver := solveInheritedScopes(a.service.options.InheritedScopesSolver)
disable := middleware.Disable(a.ac.IsDisabled())
a.router.Group(fmt.Sprintf("/api/access-control/%s", a.service.options.Resource), func(r routing.RouteRegister) {
actionRead := fmt.Sprintf("%s.permissions:read", a.service.options.Resource)
actionWrite := fmt.Sprintf("%s.permissions:write", a.service.options.Resource)
scope := accesscontrol.Scope(a.service.options.Resource, a.service.options.ResourceAttribute, accesscontrol.Parameter(":resourceID"))
actionWrite, actionRead := fmt.Sprintf("%s.permissions:write", a.service.options.Resource), fmt.Sprintf("%s.permissions:read", a.service.options.Resource)
readEvaluator, writeEvaluator := a.getEvaluators(actionRead, actionWrite, scope)
r.Get("/description", auth(disable, accesscontrol.EvalPermission(actionRead)), routing.Wrap(a.getDescription))
r.Get("/:resourceID", uidSolver, auth(disable, accesscontrol.EvalPermission(actionRead, scope)), routing.Wrap(a.getPermissions))
r.Post("/:resourceID/users/:userID", uidSolver, auth(disable, accesscontrol.EvalPermission(actionWrite, scope)), routing.Wrap(a.setUserPermission))
r.Post("/:resourceID/teams/:teamID", uidSolver, auth(disable, accesscontrol.EvalPermission(actionWrite, scope)), routing.Wrap(a.setTeamPermission))
r.Post("/:resourceID/builtInRoles/:builtInRole", uidSolver, auth(disable, accesscontrol.EvalPermission(actionWrite, scope)), routing.Wrap(a.setBuiltinRolePermission))
r.Get("/:resourceID", inheritanceSolver, uidSolver, auth(disable, readEvaluator), routing.Wrap(a.getPermissions))
r.Post("/:resourceID/users/:userID", inheritanceSolver, uidSolver, auth(disable, writeEvaluator), routing.Wrap(a.setUserPermission))
r.Post("/:resourceID/teams/:teamID", inheritanceSolver, uidSolver, auth(disable, writeEvaluator), routing.Wrap(a.setTeamPermission))
r.Post("/:resourceID/builtInRoles/:builtInRole", inheritanceSolver, uidSolver, auth(disable, writeEvaluator), routing.Wrap(a.setBuiltinRolePermission))
})
}
@@ -167,16 +167,7 @@ func TestApi_getPermissions(t *testing.T) {
assert.Equal(t, tt.expectedStatus, recorder.Code)
if tt.expectedStatus == http.StatusOK {
assert.Len(t, permissions, 3, "expected three assignments: user, team, builtin")
for _, p := range permissions {
if p.UserID != 0 {
assert.Equal(t, "View", p.Permission)
} else if p.TeamID != 0 {
assert.Equal(t, "Edit", p.Permission)
} else {
assert.Equal(t, "Edit", p.Permission)
}
}
checkSeededPermissions(t, permissions)
}
})
}
@@ -468,18 +459,7 @@ func TestApi_UidSolver(t *testing.T) {
assert.Equal(t, tt.expectedStatus, recorder.Code)
if tt.expectedStatus == http.StatusOK {
assert.Len(t, permissions, 3, "expected three assignments: user, team, builtin")
for _, p := range permissions {
if p.UserID != 0 {
assert.Equal(t, "View", p.Permission)
} else if p.TeamID != 0 {
assert.Equal(t, "Edit", p.Permission)
} else {
assert.Equal(t, "Edit", p.Permission)
}
}
} else {
assert.Equal(t, tt.expectedStatus, recorder.Code)
checkSeededPermissions(t, permissions)
}
})
}
@@ -490,6 +470,66 @@ func withSolver(options Options, solver UidSolver) Options {
return options
}
type inheritSolverTestCase struct {
desc string
resourceID string
expectedStatus int
}
func TestApi_InheritSolver(t *testing.T) {
tests := []inheritSolverTestCase{
{
desc: "expect parents permission to apply",
resourceID: "resourceID",
expectedStatus: http.StatusOK,
},
{
desc: "expect direct permissions to apply (no inheritance)",
resourceID: "orphanedID",
expectedStatus: http.StatusOK,
},
{
desc: "expect 404 when resource is not found",
resourceID: "notfound",
expectedStatus: http.StatusNotFound,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
userPermissions := []*accesscontrol.Permission{
{Action: "dashboards.permissions:read", Scope: "parents:id:parentID"}, // Inherited permission
{Action: "dashboards.permissions:read", Scope: "dashboards:id:orphanedID"}, // Direct permission
{Action: accesscontrol.ActionTeamsRead, Scope: accesscontrol.ScopeTeamsAll},
{Action: accesscontrol.ActionOrgUsersRead, Scope: accesscontrol.ScopeUsersAll},
}
// Add the inheritance solver "resourceID -> [parentID]" "orphanedID -> []"
service, sql := setupTestEnvironment(t, userPermissions,
withInheritance(testOptions, testInheritedScopeSolver, testInheritedScopePrefixes),
)
server := setupTestServer(t, &models.SignedInUser{OrgId: 1, Permissions: map[int64]map[string][]string{
1: accesscontrol.GroupScopesByAction(userPermissions),
}}, service)
// Seed permissions for users/teams/built-in roles specific to the test case resourceID
seedPermissions(t, tt.resourceID, sql, service)
permissions, recorder := getPermission(t, server, testOptions.Resource, tt.resourceID)
require.Equal(t, tt.expectedStatus, recorder.Code)
if tt.expectedStatus == http.StatusOK {
checkSeededPermissions(t, permissions)
}
})
}
}
func withInheritance(options Options, solver InheritedScopesSolver, inheritedPrefixes []string) Options {
options.InheritedScopesSolver = solver
options.InheritedScopePrefixes = inheritedPrefixes
return options
}
func setupTestServer(t *testing.T, user *models.SignedInUser, service *Service) *web.Mux {
server := web.New()
server.UseMiddleware(web.Renderer(path.Join(setting.StaticRootPath, "views"), "[[", "]]"))
@@ -530,6 +570,17 @@ var testOptions = Options{
},
}
var testInheritedScopePrefixes = []string{"parents:id:"}
var testInheritedScopeSolver = func(ctx context.Context, orgID int64, id string) ([]string, error) {
if id == "resourceID" { // Has parent
return []string{"parents:id:parentID"}, nil
}
if id == "orphanedID" { // Exists but with no parent
return nil, nil
}
return nil, errors.New("not found")
}
var testSolver = func(ctx context.Context, orgID int64, uid string) (int64, error) {
if uid == "resourceUID" {
return 1, nil
@@ -561,6 +612,19 @@ func setPermission(t *testing.T, server *web.Mux, resource, resourceID, permissi
return recorder
}
func checkSeededPermissions(t *testing.T, permissions []resourcePermissionDTO) {
assert.Len(t, permissions, 3, "expected three assignments: user, team, builtin")
for _, p := range permissions {
if p.UserID != 0 {
assert.Equal(t, "View", p.Permission)
} else if p.TeamID != 0 {
assert.Equal(t, "Edit", p.Permission)
} else {
assert.Equal(t, "Edit", p.Permission)
}
}
}
func seedPermissions(t *testing.T, resourceID string, sql *sqlstore.SQLStore, service *Service) {
t.Helper()
// seed team 1 with "Edit" permission on dashboard 1
@@ -5,6 +5,7 @@ import (
"strconv"
"github.com/grafana/grafana/pkg/models"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
@@ -23,3 +24,22 @@ func solveUID(solve UidSolver) web.Handler {
}
}
}
// solveInheritedScopes will add the inherited scopes to the context param by prefix
// Ex: params["folders:uid:"] = "folders:uid:BCeknZL7k"
func solveInheritedScopes(solve InheritedScopesSolver) web.Handler {
return func(c *models.ReqContext) {
if solve != nil && util.IsValidShortUID(web.Params(c.Req)[":resourceID"]) {
params := web.Params(c.Req)
scopes, err := solve(c.Req.Context(), c.OrgId, params[":resourceID"])
if err != nil {
c.JsonApiErr(http.StatusNotFound, "Resource not found", err)
return
}
for _, scope := range scopes {
params[ac.ScopePrefix(scope)] = scope
}
web.SetURLParams(c.Req, params)
}
}
}
@@ -42,4 +42,6 @@ type Options struct {
UidSolver UidSolver
// InheritedScopesSolver if configured can generate additional scopes that will be used when fetching permissions for a resource
InheritedScopesSolver InheritedScopesSolver
// InheritedScopePrefixes if configured are used to create evaluators with the scopes returned by InheritedScopesSolver
InheritedScopePrefixes []string
}
+47 -58
View File
@@ -16,7 +16,7 @@ type RoleRegistry interface {
// Roles definition
var (
ldapReaderRole = RoleDTO{
Name: ldapReader,
Name: "fixed:ldap:reader",
DisplayName: "LDAP reader",
Description: "Read LDAP configuration and status.",
Group: "LDAP",
@@ -32,7 +32,7 @@ var (
}
ldapWriterRole = RoleDTO{
Name: ldapWriter,
Name: "fixed:ldap:writer",
DisplayName: "LDAP writer",
Description: "Read and update LDAP configuration and read LDAP status.",
Group: "LDAP",
@@ -48,7 +48,7 @@ var (
}
orgUsersWriterRole = RoleDTO{
Name: orgUsersWriter,
Name: "fixed:org.users:writer",
DisplayName: "Organization user writer",
Description: "Within a single organization, add a user, invite a user, read information about a user and their role, remove a user from that organization, or change the role of a user.",
Group: "User administration (organizational)",
@@ -70,7 +70,7 @@ var (
}
orgUsersReaderRole = RoleDTO{
Name: orgUsersReader,
Name: "fixed:org.users:reader",
DisplayName: "Organization user reader",
Description: "Read users within a single organization.",
Group: "User administration (organizational)",
@@ -83,12 +83,12 @@ var (
},
}
settingsReaderRole = RoleDTO{
Version: 4,
SettingsReaderRole = RoleDTO{
Name: "fixed:settings:reader",
DisplayName: "Setting reader",
Description: "Read Grafana instance settings.",
Group: "Settings",
Name: settingsReader,
Version: 4,
Permissions: []Permission{
{
Action: ActionSettingsRead,
@@ -98,11 +98,11 @@ var (
}
statsReaderRole = RoleDTO{
Version: 3,
Name: statsReader,
Name: "fixed:stats:reader",
DisplayName: "Statistics reader",
Description: "Read Grafana instance statistics.",
Group: "Statistics",
Version: 3,
Permissions: []Permission{
{
Action: ActionServerStatsRead,
@@ -111,7 +111,7 @@ var (
}
usersReaderRole = RoleDTO{
Name: usersReader,
Name: "fixed:users:reader",
DisplayName: "User reader",
Description: "Read all users and their information, such as team memberships, authentication tokens, and quotas.",
Group: "User administration (global)",
@@ -137,7 +137,7 @@ var (
}
usersWriterRole = RoleDTO{
Name: usersWriter,
Name: "fixed:users:writer",
DisplayName: "User writer",
Description: "Read and update all attributes and settings for all users in Grafana: update user information, read user information, create or enable or disable a user, make a user a Grafana administrator, sign out a user, update a user’s authentication token, or update quotas for all users.",
Group: "User administration (global)",
@@ -186,55 +186,44 @@ var (
}
)
// Role names definitions
const (
ldapReader = "fixed:ldap:reader"
ldapWriter = "fixed:ldap:writer"
orgUsersReader = "fixed:org.users:reader"
orgUsersWriter = "fixed:org.users:writer"
settingsReader = "fixed:settings:reader"
statsReader = "fixed:stats:reader"
usersReader = "fixed:users:reader"
usersWriter = "fixed:users:writer"
)
var (
// FixedRoles provides a map of permission sets/roles which can be
// assigned to a set of users. When adding a new resource protected by
// Grafana access control the default permissions should be added to a
// new fixed role in this set so that users can access the new
// resource. FixedRoleGrants lists which built-in roles are
// assigned which fixed roles in this list.
FixedRoles = map[string]RoleDTO{
ldapReader: ldapReaderRole,
ldapWriter: ldapWriterRole,
orgUsersReader: orgUsersReaderRole,
orgUsersWriter: orgUsersWriterRole,
settingsReader: settingsReaderRole,
statsReader: statsReaderRole,
usersReader: usersReaderRole,
usersWriter: usersWriterRole,
// Declare OSS roles to the accesscontrol service
func DeclareFixedRoles(ac AccessControl) error {
ldapReader := RoleRegistration{
Role: ldapReaderRole,
Grants: []string{RoleGrafanaAdmin},
}
ldapWriter := RoleRegistration{
Role: ldapWriterRole,
Grants: []string{RoleGrafanaAdmin},
}
orgUsersReader := RoleRegistration{
Role: orgUsersReaderRole,
Grants: []string{RoleGrafanaAdmin, string(models.ROLE_ADMIN)},
}
orgUsersWriter := RoleRegistration{
Role: orgUsersWriterRole,
Grants: []string{RoleGrafanaAdmin, string(models.ROLE_ADMIN)},
}
settingsReader := RoleRegistration{
Role: SettingsReaderRole,
Grants: []string{RoleGrafanaAdmin},
}
statsReader := RoleRegistration{
Role: statsReaderRole,
Grants: []string{RoleGrafanaAdmin},
}
usersReader := RoleRegistration{
Role: usersReaderRole,
Grants: []string{RoleGrafanaAdmin},
}
usersWriter := RoleRegistration{
Role: usersWriterRole,
Grants: []string{RoleGrafanaAdmin},
}
// FixedRoleGrants specifies which built-in roles are assigned
// to which set of FixedRoles by default. Alphabetically sorted.
FixedRoleGrants = map[string][]string{
RoleGrafanaAdmin: {
ldapReader,
ldapWriter,
orgUsersReader,
orgUsersWriter,
settingsReader,
statsReader,
usersReader,
usersWriter,
},
string(models.ROLE_ADMIN): {
orgUsersReader,
orgUsersWriter,
},
}
)
return ac.DeclareFixedRoles(ldapReader, ldapWriter, orgUsersReader, orgUsersWriter,
settingsReader, statsReader, usersReader, usersWriter)
}
func ConcatPermissions(permissions ...[]Permission) []Permission {
if permissions == nil {
-30
View File
@@ -1,41 +1,11 @@
package accesscontrol
import (
"sort"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestFixedRoles(t *testing.T) {
for name, role := range FixedRoles {
assert.Truef(t,
strings.HasPrefix(name, "fixed:"),
"expected all fixed roles to be prefixed by 'fixed:', found role '%s'", name,
)
assert.Equal(t, name, role.Name)
assert.NotZero(t, role.Version)
}
}
func TestFixedRoleGrants(t *testing.T) {
for _, grants := range FixedRoleGrants {
// Check grants list is sorted
assert.True(t,
sort.SliceIsSorted(grants, func(i, j int) bool {
return grants[i] < grants[j]
}),
"require role grant lists to be sorted",
)
// Check all granted roles have been registered
for _, r := range grants {
assert.Contains(t, FixedRoles, r)
}
}
}
func TestConcatPermissions(t *testing.T) {
perms1 := []Permission{
{
+3 -3
View File
@@ -143,7 +143,7 @@ func (s *ScopeResolver) GetResolveAttributeScopeMutator(orgID int64) ScopeMutato
var err error
// By default the scope remains unchanged
resolvedScope := scope
prefix := scopePrefix(scope)
prefix := ScopePrefix(scope)
if fn, ok := s.attributeResolvers[prefix]; ok {
resolvedScope, err = fn(ctx, orgID, scope)
if err != nil {
@@ -157,10 +157,10 @@ func (s *ScopeResolver) GetResolveAttributeScopeMutator(orgID int64) ScopeMutato
}
}
// scopePrefix returns the prefix associated to a given scope
// ScopePrefix returns the prefix associated to a given scope
// we assume prefixes are all in the form <resource>:<attribute>:<value>
// ex: "datasources:name:test" returns "datasources:name:"
func scopePrefix(scope string) string {
func ScopePrefix(scope string) string {
parts := strings.Split(scope, ":")
// We assume prefixes don't have more than maxPrefixParts parts
if len(parts) > maxPrefixParts {
+1 -1
View File
@@ -198,7 +198,7 @@ func Test_scopePrefix(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
prefix := scopePrefix(tt.scope)
prefix := ScopePrefix(tt.scope)
assert.Equal(t, tt.want, prefix)
})
+1 -1
View File
@@ -73,7 +73,7 @@ func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64)
}
}
if err := bus.Dispatch(ctx.Req.Context(), &query); err != nil {
if err := h.SQLStore.GetSignedInUserWithCacheCtx(ctx.Req.Context(), &query); err != nil {
if errors.Is(err, models.ErrUserNotFound) {
ctx.Logger.Debug(
"Failed to find user using JWT claims",
@@ -35,7 +35,7 @@ const (
const ServiceName = "ContextHandler"
func ProvideService(cfg *setting.Cfg, tokenService models.UserTokenService, jwtService models.JWTService,
remoteCache *remotecache.RemoteCache, renderService rendering.Service, sqlStore *sqlstore.SQLStore,
remoteCache *remotecache.RemoteCache, renderService rendering.Service, sqlStore sqlstore.Store,
tracer tracing.Tracer, authProxy *authproxy.AuthProxy) *ContextHandler {
return &ContextHandler{
Cfg: cfg,
@@ -151,7 +151,7 @@ func (h *ContextHandler) Middleware(mContext *web.Context) {
// update last seen every 5min
if reqContext.ShouldUpdateLastSeenAt() {
reqContext.Logger.Debug("Updating last user_seen_at", "user_id", reqContext.UserId)
if err := bus.Dispatch(mContext.Req.Context(), &models.UpdateUserLastSeenAtCommand{UserId: reqContext.UserId}); err != nil {
if err := h.SQLStore.UpdateUserLastSeenAt(mContext.Req.Context(), &models.UpdateUserLastSeenAtCommand{UserId: reqContext.UserId}); err != nil {
reqContext.Logger.Error("Failed to update last_seen_at", "error", err)
}
}
@@ -209,7 +209,7 @@ func (h *ContextHandler) initContextWithAPIKey(reqContext *models.ReqContext) bo
// fetch key
keyQuery := models.GetApiKeyByNameQuery{KeyName: decoded.Name, OrgId: decoded.OrgId}
if err := bus.Dispatch(reqContext.Req.Context(), &keyQuery); err != nil {
if err := h.SQLStore.GetApiKeyByName(reqContext.Req.Context(), &keyQuery); err != nil {
reqContext.JsonApiErr(401, InvalidAPIKey, err)
return true
}
@@ -251,7 +251,7 @@ func (h *ContextHandler) initContextWithAPIKey(reqContext *models.ReqContext) bo
//Use service account linked to API key as the signed in user
query := models.GetSignedInUserQuery{UserId: *apikey.ServiceAccountId, OrgId: apikey.OrgId}
if err := bus.Dispatch(reqContext.Req.Context(), &query); err != nil {
if err := h.SQLStore.GetSignedInUserWithCacheCtx(reqContext.Req.Context(), &query); err != nil {
reqContext.Logger.Error(
"Failed to link API key to service account in",
"id", query.UserId,
@@ -308,7 +308,7 @@ func (h *ContextHandler) initContextWithBasicAuth(reqContext *models.ReqContext,
user := authQuery.User
query := models.GetSignedInUserQuery{UserId: user.Id, OrgId: orgID}
if err := bus.Dispatch(ctx, &query); err != nil {
if err := h.SQLStore.GetSignedInUserWithCacheCtx(ctx, &query); err != nil {
reqContext.Logger.Error(
"Failed at user signed in",
"id", user.Id,
@@ -344,7 +344,7 @@ func (h *ContextHandler) initContextWithToken(reqContext *models.ReqContext, org
}
query := models.GetSignedInUserQuery{UserId: token.UserId, OrgId: orgID}
if err := bus.Dispatch(ctx, &query); err != nil {
if err := h.SQLStore.GetSignedInUserWithCacheCtx(ctx, &query); err != nil {
reqContext.Logger.Error("Failed to get user with id", "userId", token.UserId, "error", err)
return false
}
+1
View File
@@ -9,6 +9,7 @@ import (
type AuthInfoService interface {
LookupAndUpdate(ctx context.Context, query *models.GetUserByAuthInfoQuery) (*models.User, error)
GetAuthInfo(ctx context.Context, query *models.GetAuthInfoQuery) error
GetExternalUserInfoByLogin(ctx context.Context, query *models.GetExternalUserInfoByLoginQuery) error
SetAuthInfo(ctx context.Context, cmd *models.SetAuthInfoCommand) error
UpdateAuthInfo(ctx context.Context, cmd *models.UpdateAuthInfoCommand) error
}
@@ -183,3 +183,7 @@ func (s *Implementation) UpdateAuthInfo(ctx context.Context, cmd *models.UpdateA
func (s *Implementation) SetAuthInfo(ctx context.Context, cmd *models.SetAuthInfoCommand) error {
return s.authInfoStore.SetAuthInfo(ctx, cmd)
}
func (s *Implementation) GetExternalUserInfoByLogin(ctx context.Context, query *models.GetExternalUserInfoByLoginQuery) error {
return s.authInfoStore.GetExternalUserInfoByLogin(ctx, query)
}
+1
View File
@@ -18,5 +18,6 @@ type TeamSyncFunc func(user *models.User, externalUser *models.ExternalUserInfo)
type Service interface {
CreateUser(cmd models.CreateUserCommand) (*models.User, error)
UpsertUser(ctx context.Context, cmd *models.UpsertUserCommand) error
DisableExternalUser(ctx context.Context, username string) error
SetTeamSyncFunc(TeamSyncFunc)
}
@@ -130,6 +130,46 @@ func (ls *Implementation) UpsertUser(ctx context.Context, cmd *models.UpsertUser
return nil
}
func (ls *Implementation) DisableExternalUser(ctx context.Context, username string) error {
// Check if external user exist in Grafana
userQuery := &models.GetExternalUserInfoByLoginQuery{
LoginOrEmail: username,
}
if err := ls.AuthInfoService.GetExternalUserInfoByLogin(ctx, userQuery); err != nil {
return err
}
userInfo := userQuery.Result
if userInfo.IsDisabled {
return nil
}
logger.Debug(
"Disabling external user",
"user",
userQuery.Result.Login,
)
// Mark user as disabled in grafana db
disableUserCmd := &models.DisableUserCommand{
UserId: userQuery.Result.UserId,
IsDisabled: true,
}
if err := ls.SQLStore.DisableUser(ctx, disableUserCmd); err != nil {
logger.Debug(
"Error disabling external user",
"user",
userQuery.Result.Login,
"message",
err.Error(),
)
return err
}
return nil
}
// SetTeamSyncFunc sets the function received through args as the team sync function.
func (ls *Implementation) SetTeamSyncFunc(teamSyncFunc login.TeamSyncFunc) {
ls.TeamSync = teamSyncFunc
@@ -45,3 +45,7 @@ func (s LoginServiceMock) UpsertUser(ctx context.Context, cmd *models.UpsertUser
cmd.Result = s.ExpectedUser
return s.ExpectedError
}
func (s LoginServiceMock) DisableExternalUser(ctx context.Context, username string) error {
return nil
}
@@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log/level"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/login/logintest"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/stretchr/testify/assert"
@@ -19,7 +20,7 @@ import (
func Test_syncOrgRoles_doesNotBreakWhenTryingToRemoveLastOrgAdmin(t *testing.T) {
user := createSimpleUser()
externalUser := createSimpleExternalUser()
authInfoMock := &authInfoServiceMock{}
authInfoMock := &logintest.AuthInfoServiceFake{}
store := &mockstore.SQLStoreMock{
ExpectedUserOrgList: createUserOrgDTO(),
@@ -44,7 +45,7 @@ func Test_syncOrgRoles_whenTryingToRemoveLastOrgLogsError(t *testing.T) {
user := createSimpleUser()
externalUser := createSimpleExternalUser()
authInfoMock := &authInfoServiceMock{}
authInfoMock := &logintest.AuthInfoServiceFake{}
store := &mockstore.SQLStoreMock{
ExpectedUserOrgList: createUserOrgDTO(),
@@ -63,29 +64,8 @@ func Test_syncOrgRoles_whenTryingToRemoveLastOrgLogsError(t *testing.T) {
assert.Contains(t, buf.String(), models.ErrLastOrgAdmin.Error())
}
type authInfoServiceMock struct {
user *models.User
err error
}
func (a *authInfoServiceMock) LookupAndUpdate(ctx context.Context, query *models.GetUserByAuthInfoQuery) (*models.User, error) {
return a.user, a.err
}
func (a *authInfoServiceMock) GetAuthInfo(ctx context.Context, query *models.GetAuthInfoQuery) error {
return nil
}
func (a *authInfoServiceMock) SetAuthInfo(ctx context.Context, cmd *models.SetAuthInfoCommand) error {
return nil
}
func (a *authInfoServiceMock) UpdateAuthInfo(ctx context.Context, cmd *models.UpdateAuthInfoCommand) error {
return nil
}
func Test_teamSync(t *testing.T) {
authInfoMock := &authInfoServiceMock{}
authInfoMock := &logintest.AuthInfoServiceFake{}
login := Implementation{
Bus: bus.New(),
QuotaService: &quota.QuotaService{},
@@ -99,7 +79,7 @@ func Test_teamSync(t *testing.T) {
Name: "test_user",
Login: "test_user",
}
authInfoMock.user = expectedUser
authInfoMock.ExpectedUser = expectedUser
bus.ClearBusHandlers()
t.Cleanup(func() { bus.ClearBusHandlers() })

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