Merge remote-tracking branch 'origin/main' into guicaulada/unified-secrets
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
...
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 =
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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" >}})
|
||||
|
||||
+10
-10
@@ -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.
|
||||
+12
-12
@@ -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.
|
||||
+10
-10
@@ -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.
|
||||
|
||||

|
||||

|
||||
|
||||
{{< 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.
|
||||
|
||||

|
||||

|
||||
@@ -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" >}}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
@@ -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' }]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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" >}}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -40,6 +40,7 @@ type AnnotationPermission struct {
|
||||
}
|
||||
|
||||
type AnnotationActions struct {
|
||||
CanAdd bool `json:"canAdd"`
|
||||
CanEdit bool `json:"canEdit"`
|
||||
CanDelete bool `json:"canDelete"`
|
||||
}
|
||||
|
||||
+11
-1
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,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()
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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, ¯oRole.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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{
|
||||
{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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: "a.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
Reference in New Issue
Block a user